Merge "Add a new CanvasEngine, gated by flag" into tm-qpr-dev

This commit is contained in:
Aurélien Pomini
2022-09-22 16:31:40 +00:00
committed by Android (Google) Code Review
4 changed files with 568 additions and 3 deletions

View File

@@ -16,12 +16,21 @@
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.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;
@@ -31,16 +40,22 @@ 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;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.wallpapers.canvas.ImageCanvasWallpaperRenderer;
import com.android.systemui.wallpapers.gl.EglHelper;
import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -59,16 +74,19 @@ public class ImageWallpaper extends WallpaperService {
private static final @android.annotation.NonNull RectF LOCAL_COLOR_BOUNDS =
new RectF(0, 0, 1, 1);
private static final boolean DEBUG = false;
private final ArrayList<RectF> mLocalColorsToAdd = new ArrayList<>();
private final ArraySet<RectF> mColorAreas = new ArraySet<>();
private volatile int mPages = 1;
private HandlerThread mWorker;
// scaled down version
private Bitmap mMiniBitmap;
private final FeatureFlags mFeatureFlags;
@Inject
public ImageWallpaper() {
public ImageWallpaper(FeatureFlags featureFlags) {
super();
mFeatureFlags = featureFlags;
}
@Override
@@ -80,7 +98,7 @@ public class ImageWallpaper extends WallpaperService {
@Override
public Engine onCreateEngine() {
return new GLEngine();
return mFeatureFlags.isEnabled(USE_CANVAS_RENDERER) ? new CanvasEngine() : new GLEngine();
}
@Override
@@ -489,4 +507,270 @@ public class ImageWallpaper extends WallpaperService {
mRenderer.dump(prefix, fd, out, args);
}
}
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 Bitmap mBitmap;
private Display mDisplay;
private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
private AsyncTask<Void, Void, Bitmap> mLoader;
private boolean mNeedsDrawAfterLoadingWallpaper = false;
CanvasEngine() {
super();
setFixedSizeAllowed(true);
setShowForAllUsers(true);
}
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 onCreate(SurfaceHolder surfaceHolder) {
if (DEBUG) {
Log.d(TAG, "onCreate");
}
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);
}
@Override
public void onDestroy() {
super.onDestroy();
unloadWallpaper();
}
@Override
public boolean shouldZoomOutWallpaper() {
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);
}
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
super.onSurfaceCreated(holder);
if (DEBUG) {
Log.i(TAG, "onSurfaceCreated");
}
mImageCanvasWallpaperRenderer.setSurfaceHolder(holder);
}
@Override
public void onSurfaceRedrawNeeded(SurfaceHolder holder) {
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);
}
private DisplayInfo getDisplayInfo() {
mDisplay.getDisplayInfo(mTmpDisplayInfo);
return mTmpDisplayInfo;
}
private void drawFrame(boolean forceRedraw) {
if (!mImageCanvasWallpaperRenderer.isSurfaceHolderLoaded()) {
Log.e(TAG, "attempt to draw a frame without a valid surface");
return;
}
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());
} else {
if (mBitmap != null) {
mBitmap.recycle();
}
mBitmap = bitmap;
}
}
private 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 unloadWallpaper() {
if (mLoader != null) {
mLoader.cancel(false);
mLoader = null;
}
if (mBitmap != null) {
mBitmap.recycle();
}
mBitmap = null;
final Surface surface = getSurfaceHolder().getSurface();
surface.hwuiDestroy();
mWallpaperManager.forgetLoadedWallpaper();
}
private void scheduleUnloadWallpaper() {
Handler handler = getMainThreadHandler();
handler.removeCallbacks(mUnloadWallpaperCallback);
handler.postDelayed(mUnloadWallpaperCallback, DELAY_FORGET_WALLPAPER);
}
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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

@@ -41,6 +41,7 @@ import android.view.DisplayInfo;
import android.view.SurfaceHolder;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
import org.junit.Before;
@@ -72,6 +73,8 @@ public class ImageWallpaperTest extends SysuiTestCase {
private Bitmap mWallpaperBitmap;
@Mock
private Handler mHandler;
@Mock
private FeatureFlags mFeatureFlags;
private CountDownLatch mEventCountdown;
@@ -100,7 +103,7 @@ public class ImageWallpaperTest extends SysuiTestCase {
}
private ImageWallpaper createImageWallpaper() {
return new ImageWallpaper() {
return new ImageWallpaper(mFeatureFlags) {
@Override
public Engine onCreateEngine() {
return new GLEngine(mHandler) {

View File

@@ -0,0 +1,133 @@
/*
* 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
);
}
}