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

Commit 4e404406 authored by Ahan Wu's avatar Ahan Wu
Browse files

Fix ImageWallpaper memory regression

Scale bitmap to fit display size leads to memory regression, this cl
removes the sacling logic and also disable wallpaper transitions when
the bitmap size is smaller than display size to avoid broken visual.

Bug: 147379974
Bug: 145897588
Bug: 124838911
Test: Manually
Test: atest com.android.systemui.ImageWallpaperTest
--rerun-until-failure 20
Test: atest com.android.systemui
Change-Id: I243274af54538fc89268c448aa2c5a95f63c7ae3
parent 7adf2907
Loading
Loading
Loading
Loading
+51 −13
Original line number Diff line number Diff line
@@ -18,12 +18,14 @@ package com.android.systemui;

import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.HandlerThread;
import android.os.Trace;
import android.service.wallpaper.WallpaperService;
import android.util.Log;
import android.util.Size;
import android.view.DisplayInfo;
import android.view.SurfaceHolder;

import com.android.internal.annotations.VisibleForTesting;
@@ -93,14 +95,20 @@ public class ImageWallpaper extends WallpaperService {
        private StatusBarStateController mController;
        private final Runnable mFinishRenderingTask = this::finishRendering;
        private final boolean mNeedTransition;
        private boolean mShouldStopTransition;
        @VisibleForTesting
        final boolean mIsHighEndGfx;
        private final boolean mDisplayNeedsBlanking;
        private final DisplayInfo mDisplayInfo = new DisplayInfo();
        private final Object mMonitor = new Object();
        private boolean mNeedRedraw;
        // This variable can only be accessed in synchronized block.
        private boolean mWaitingForRendering;

        GLEngine(Context context, DozeParameters dozeParameters) {
            mNeedTransition = ActivityManager.isHighEndGfx()
                    && !dozeParameters.getDisplayNeedsBlanking();
            mIsHighEndGfx = ActivityManager.isHighEndGfx();
            mDisplayNeedsBlanking = dozeParameters.getDisplayNeedsBlanking();
            mNeedTransition = mIsHighEndGfx && !mDisplayNeedsBlanking;

            // We will preserve EGL context when we are in lock screen or aod
            // to avoid janking in following transition, we need to release when back to home.
@@ -112,14 +120,23 @@ public class ImageWallpaper extends WallpaperService {

        @Override
        public void onCreate(SurfaceHolder surfaceHolder) {
            mEglHelper = new EglHelper();
            mEglHelper = getEglHelperInstance();
            // Deferred init renderer because we need to get wallpaper by display context.
            mRenderer = new ImageWallpaperRenderer(getDisplayContext(), this /* SurfaceProxy */);
            mRenderer = getRendererInstance();
            getDisplayContext().getDisplay().getDisplayInfo(mDisplayInfo);
            setFixedSizeAllowed(true);
            setOffsetNotificationsEnabled(true);
            updateSurfaceSize();
        }

        EglHelper getEglHelperInstance() {
            return new EglHelper();
        }

        ImageWallpaperRenderer getRendererInstance() {
            return new ImageWallpaperRenderer(getDisplayContext(), this /* SurfaceProxy */);
        }

        private void updateSurfaceSize() {
            SurfaceHolder holder = getSurfaceHolder();
            Size frameSize = mRenderer.reportSurfaceSize();
@@ -128,6 +145,26 @@ public class ImageWallpaper extends WallpaperService {
            holder.setFixedSize(width, height);
        }

        /**
         * Check if necessary to stop transition with current wallpaper on this device. <br/>
         * This should only be invoked after {@link #onSurfaceCreated(SurfaceHolder)}}
         * is invoked since it needs display context and surface frame size.
         * @return true if need to stop transition.
         */
        @VisibleForTesting
        boolean checkIfShouldStopTransition() {
            int orientation = getDisplayContext().getResources().getConfiguration().orientation;
            Rect frame = getSurfaceHolder().getSurfaceFrame();
            Rect display = new Rect();
            if (orientation == Configuration.ORIENTATION_PORTRAIT) {
                display.set(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
            } else {
                display.set(0, 0, mDisplayInfo.logicalHeight, mDisplayInfo.logicalWidth);
            }
            return mNeedTransition
                    && (frame.width() < display.width() || frame.height() < display.height());
        }

        @Override
        public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep,
                float yOffsetStep, int xPixelOffset, int yPixelOffset) {
@@ -137,12 +174,14 @@ public class ImageWallpaper extends WallpaperService {
        @Override
        public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) {
            if (!mNeedTransition) return;
            final long duration = mShouldStopTransition ? 0 : animationDuration;
            if (DEBUG) {
                Log.d(TAG, "onAmbientModeChanged: inAmbient=" + inAmbientMode
                        + ", duration=" + animationDuration);
                        + ", duration=" + duration
                        + ", mShouldStopTransition=" + mShouldStopTransition);
            }
            mWorker.getThreadHandler().post(
                    () -> mRenderer.updateAmbientMode(inAmbientMode, animationDuration));
                    () -> mRenderer.updateAmbientMode(inAmbientMode, duration));
            if (inAmbientMode && animationDuration == 0) {
                // This means that we are transiting from home to aod, to avoid
                // race condition between window visibility and transition,
@@ -183,6 +222,7 @@ public class ImageWallpaper extends WallpaperService {

        @Override
        public void onSurfaceCreated(SurfaceHolder holder) {
            mShouldStopTransition = checkIfShouldStopTransition();
            mWorker.getThreadHandler().post(() -> {
                mEglHelper.init(holder, needSupportWideColorGamut());
                mRenderer.onSurfaceCreated();
@@ -348,15 +388,13 @@ public class ImageWallpaper extends WallpaperService {
        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);

            boolean isHighEndGfx = ActivityManager.isHighEndGfx();
            out.print(prefix); out.print("isHighEndGfx="); out.println(isHighEndGfx);

            out.print(prefix); out.print("isHighEndGfx="); out.println(mIsHighEndGfx);
            out.print(prefix); out.print("displayNeedsBlanking=");
            out.println(
                    mDozeParameters != null ? mDozeParameters.getDisplayNeedsBlanking() : "null");

            out.println(mDisplayNeedsBlanking);
            out.print(prefix); out.print("displayInfo="); out.print(mDisplayInfo);
            out.print(prefix); out.print("mNeedTransition="); out.println(mNeedTransition);
            out.print(prefix); out.print("mShouldStopTransition=");
            out.println(mShouldStopTransition);
            out.print(prefix); out.print("StatusBarState=");
            out.println(mController != null ? mController.getState() : "null");

+7 −10
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ import android.util.Log;
import android.util.MathUtils;
import android.util.Size;
import android.view.DisplayInfo;
import android.view.WindowManager;

import com.android.systemui.R;

@@ -71,8 +70,7 @@ public class ImageWallpaperRenderer implements GLWallpaperRenderer,
        }

        DisplayInfo displayInfo = new DisplayInfo();
        WindowManager wm = context.getSystemService(WindowManager.class);
        wm.getDefaultDisplay().getDisplayInfo(displayInfo);
        context.getDisplay().getDisplayInfo(displayInfo);

        // We only do transition in portrait currently, b/137962047.
        int orientation = context.getResources().getConfiguration().orientation;
@@ -88,6 +86,10 @@ public class ImageWallpaperRenderer implements GLWallpaperRenderer,
        mImageProcessHelper = new ImageProcessHelper();
        mImageRevealHelper = new ImageRevealHelper(this);

        startProcessingImage();
    }

    protected void startProcessingImage() {
        if (loadBitmap()) {
            // Compute threshold of the image, this is an async work.
            mImageProcessHelper.start(mBitmap);
@@ -113,7 +115,7 @@ public class ImageWallpaperRenderer implements GLWallpaperRenderer,
        mBitmap = null;
    }

    private boolean loadBitmap() {
    protected boolean loadBitmap() {
        if (DEBUG) {
            Log.d(TAG, "loadBitmap: mBitmap=" + mBitmap);
        }
@@ -122,12 +124,7 @@ public class ImageWallpaperRenderer implements GLWallpaperRenderer,
            mWcgContent = mWallpaperManager.wallpaperSupportsWcg(WallpaperManager.FLAG_SYSTEM);
            mWallpaperManager.forgetLoadedWallpaper();
            if (mBitmap != null) {
                float scale = (float) mScissor.height() / mBitmap.getHeight();
                int surfaceHeight = Math.max(mScissor.height(), mBitmap.getHeight());
                int surfaceWidth = scale > 1f
                        ? Math.round(mBitmap.getWidth() * scale)
                        : mBitmap.getWidth();
                mSurfaceSize.set(0, 0, surfaceWidth, surfaceHeight);
                mSurfaceSize.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
            }
        }
        if (DEBUG) {
+157 −7
Original line number Diff line number Diff line
@@ -16,35 +16,185 @@

package com.android.systemui;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
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 android.app.WallpaperManager;
import android.content.Context;
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.DisplayManagerGlobal;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.util.Size;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.SurfaceHolder;

import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.glwallpaper.ImageWallpaperRenderer;
import com.android.systemui.statusbar.phone.DozeParameters;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.concurrent.CountDownLatch;

@SmallTest
@RunWith(AndroidJUnit4.class)
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class ImageWallpaperTest extends SysuiTestCase {
    private static final int LOW_BMP_WIDTH = 128;
    private static final int LOW_BMP_HEIGHT = 128;
    private static final int INVALID_BMP_WIDTH = 1;
    private static final int INVALID_BMP_HEIGHT = 1;
    private static final int DISPLAY_WIDTH = 1920;
    private static final int DISPLAY_HEIGHT = 1080;

    @Mock
    private SurfaceHolder mSurfaceHolder;
    @Mock
    private Context mMockContext;
    @Mock
    private Bitmap mWallpaperBitmap;
    @Mock
    private DozeParameters mDozeParam;

    private CountDownLatch mEventCountdown;
    private CountDownLatch mAmbientEventCountdown;

    @Before
    public void setUp() throws Exception {
        com.android.systemui.util.Assert.sMainLooper = TestableLooper.get(this).getLooper();
        MockitoAnnotations.initMocks(this);
        mEventCountdown = new CountDownLatch(1);
        mAmbientEventCountdown = new CountDownLatch(2);

        WallpaperManager wallpaperManager = mock(WallpaperManager.class);
        Resources resources = mock(Resources.class);

        when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(wallpaperManager);
        when(mMockContext.getResources()).thenReturn(resources);
        when(resources.getConfiguration()).thenReturn(mock(Configuration.class));

        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);
        when(mDozeParam.getDisplayNeedsBlanking()).thenReturn(false);
    }

    private ImageWallpaper createImageWallpaper() {
        return new ImageWallpaper(mDozeParam) {
            @Override
            public Engine onCreateEngine() {
                return new GLEngine(mMockContext, mDozeParam) {
                    @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();
                        mEventCountdown.countDown();
                    }
                };
            }
        };
    }

    private ImageWallpaperRenderer createImageWallpaperRenderer(ImageWallpaper.GLEngine engine) {
        return new ImageWallpaperRenderer(mMockContext, engine) {
            @Override
            public void startProcessingImage() {
                loadBitmap();
            }
        };
    }

    @Test
    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.
        // Finally, we assert the transition will not be stopped.
        verifySurfaceSizeAndAssertTransition(DISPLAY_WIDTH /* bmpWidth */,
                DISPLAY_WIDTH /* bmpHeight */,
                DISPLAY_WIDTH /* surfaceWidth */,
                DISPLAY_WIDTH /* surfaceHeight */,
                false /* assertion */);
    }

    @Test
    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.
        // Finally, we assert the transition will be stopped.
        verifySurfaceSizeAndAssertTransition(LOW_BMP_WIDTH /* bmpWidth */,
                LOW_BMP_HEIGHT /* bmpHeight */,
                LOW_BMP_WIDTH /* surfaceWidth */,
                LOW_BMP_HEIGHT /* surfaceHeight */,
                true /* assertion */);
    }

    @Test
    public void testDeliversAmbientModeChanged() {
        //TODO: We need add tests for GLEngine.
    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.
        // Finally, we assert the transition will be stopped.
        verifySurfaceSizeAndAssertTransition(INVALID_BMP_WIDTH /* bmpWidth */,
                INVALID_BMP_HEIGHT /* bmpHeight */,
                ImageWallpaper.GLEngine.MIN_SURFACE_WIDTH /* surfaceWidth */,
                ImageWallpaper.GLEngine.MIN_SURFACE_HEIGHT /* surfaceHeight */,
                true /* assertion */);
    }

    // TODO: Add more test cases for GLEngine, tracing in b/124838911.
    private void verifySurfaceSizeAndAssertTransition(int bmpWidth, int bmpHeight,
            int surfaceWidth, int surfaceHeight, boolean assertion) {
        ImageWallpaper.GLEngine wallpaperEngine =
                (ImageWallpaper.GLEngine) createImageWallpaper().onCreateEngine();

        ImageWallpaper.GLEngine engineSpy = spy(wallpaperEngine);
        when(engineSpy.mIsHighEndGfx).thenReturn(true);

        when(mWallpaperBitmap.getWidth()).thenReturn(bmpWidth);
        when(mWallpaperBitmap.getHeight()).thenReturn(bmpHeight);

        ImageWallpaperRenderer renderer = createImageWallpaperRenderer(engineSpy);
        doReturn(renderer).when(engineSpy).getRendererInstance();
        engineSpy.onCreate(engineSpy.getSurfaceHolder());

        verify(mSurfaceHolder, times(1)).setFixedSize(surfaceWidth, surfaceHeight);
        assertWithMessage("setFixedSizeAllowed should have been called.").that(
                mEventCountdown.getCount()).isEqualTo(0);

        Size frameSize = renderer.reportSurfaceSize();
        Rect frame = new Rect(0, 0, frameSize.getWidth(), frameSize.getHeight());
        when(mSurfaceHolder.getSurfaceFrame()).thenReturn(frame);

        assertThat(engineSpy.checkIfShouldStopTransition()).isEqualTo(assertion);
    }
}