diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 2878ad90835bc..0f7e14374e602 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -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 mLocalColorsToAdd = new ArrayList<>(); private final ArraySet 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 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() { + @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) { + + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java new file mode 100644 index 0000000000000..fdba16ed20590 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java @@ -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); + } + } + } +} 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 e47acd8d38341..343437634b29b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java @@ -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) { 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 new file mode 100644 index 0000000000000..93f4f82239550 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java @@ -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 + ); + } +}