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

Commit fe97f28b authored by Aurélien Pomini's avatar Aurélien Pomini Committed by Android (Google) Code Review
Browse files

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

parents 755b1989 06e60f6d
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
        );
    }
}