Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 06e60f6d authored by Aurélien Pomini's avatar Aurélien Pomini
Browse files

Add a new CanvasEngine, gated by flag

The flag can be flipped in the FlagFlipper app: Theming -> Use Canvas
Renderer.
A new file, ImageCanvasWallpaperRenderer, performs the drawing of the bitmap on the surface.
The Engine class only manages the loading/unloading of wallpapers.
This Engine is widely inspired from the old DrawableEngine, but some details have been modified:
  - updateSurfaceSize() has been modified. Now, it simply calls
    surfaceHolder.setFixedSize() with the dimensions of the bitmap (or
    128 if bitmap is smaller)
  - The Trace logic has been temporarily removed
  - The method shouldZoomOutWallpaper is overriden and always returns
    true
  - A call to setShowForAllUsers(true) has been added in the constructor
  - The 'onOffsetsChanged' method does not exist anymore, offsets are
    considered to be always 0. All computations about offset, cropping, scale and
    rotation have been removed, as they are no longer necessary.
  - The 'onVisibilityChanged' and 'dump' methods do not exist anymore.
    The wallpaper is automatically unloaded after 5000ms, so it is not mandatory to
    unload when onVisibilityChanged(false) is called.
  - bitmap.recycle() is always called when the wallpaper is changed
  - A few new tests have been added

Test: manual
Test: atest ImageWallpaperTest
Bug: 243402530

Change-Id: I20bc71e6ad9158f7728f0f44367cca9b256c77cb
parent 00749dc0
Loading
Loading
Loading
Loading
+286 −2
Original line number Diff line number Diff line
@@ -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) {

        }
    }
}
+145 −0
Original line number Diff line number Diff line
/*
 * 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);
            }
        }
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -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) {
+133 −0
Original line number Diff line number Diff line
/*
 * 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
        );
    }
}