diff options
6 files changed, 1233 insertions, 458 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 0f7e14374e60..6b3beeb54c76 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -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 @@ -581,31 +606,30 @@ public class ImageWallpaper extends WallpaperService { } @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(true); + drawFrame(); } - private DisplayInfo getDisplayInfo() { - mDisplay.getDisplayInfo(mTmpDisplayInfo); - return mTmpDisplayInfo; + private void drawFrame() { + mBackgroundExecutor.execute(this::drawFrameSynchronized); } - private void drawFrame(boolean forceRedraw) { - if (!mImageCanvasWallpaperRenderer.isSurfaceHolderLoaded()) { + private void drawFrameSynchronized() { + synchronized (mLock) { + drawFrameInternal(); + } + } + + 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; + loadWallpaperAndDrawFrameInternal(); + } else { + mBitmapUsages++; + + // drawing is done on the main thread + mMainExecutor.execute(() -> { + drawFrameOnCanvas(mBitmap); + reportEngineShown(false); + unloadBitmapIfNotUsed(); + }); } - 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()); - } else { - if (mBitmap != null) { - mBitmap.recycle(); + @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); } - mBitmap = bitmap; } + Trace.endSection(); } - private boolean isBitmapLoaded() { + @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 "); + private void unloadBitmapIfNotUsed() { + mBackgroundExecutor.execute(this::unloadBitmapIfNotUsedSynchronized); + } + + private void unloadBitmapIfNotUsedSynchronized() { + synchronized (mLock) { + mBitmapUsages -= 1; + if (mBitmapUsages <= 0) { + mBitmapUsages = 0; + unloadBitmapInternal(); } - 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 unloadBitmap() { + mBackgroundExecutor.execute(this::unloadBitmapSynchronized); } - private void unloadWallpaper() { - if (mLoader != null) { - mLoader.cancel(false); - mLoader = null; + 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 onDisplayRemoved(int displayId) { + + } + + @Override 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 - public void onDisplayRemoved(int displayId) { + 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); } } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java deleted file mode 100644 index fdba16ed2059..000000000000 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java +++ /dev/null @@ -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); - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java new file mode 100644 index 000000000000..e2e4555bb965 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java @@ -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()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java index 343437634b29..c2543589bfb8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java @@ -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; @@ -66,44 +82,86 @@ public class ImageWallpaperTest extends SysuiTestCase { 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); - Resources resources = mock(Resources.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); - when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(wallpaperManager); - when(mMockContext.getResources()).thenReturn(resources); - when(resources.getConfiguration()).thenReturn(mock(Configuration.class)); + // 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(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(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java deleted file mode 100644 index 93f4f8223955..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java +++ /dev/null @@ -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 - ); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java new file mode 100644 index 000000000000..76bff1d72141 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java @@ -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); + } +} |