| /* |
| * Copyright (C) 2009 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; |
| |
| import static com.android.systemui.flags.Flags.USE_CANVAS_RENDERER; |
| |
| import android.app.WallpaperColors; |
| import android.app.WallpaperManager; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| 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.Handler; |
| import android.os.HandlerThread; |
| import android.os.SystemClock; |
| import android.os.Trace; |
| import android.service.wallpaper.WallpaperService; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.MathUtils; |
| import android.util.Size; |
| 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.dagger.qualifiers.Background; |
| import com.android.systemui.dagger.qualifiers.Main; |
| import com.android.systemui.flags.FeatureFlags; |
| import com.android.systemui.util.concurrency.DelayableExecutor; |
| import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor; |
| 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; |
| import java.util.concurrent.Executor; |
| |
| import javax.inject.Inject; |
| |
| /** |
| * Default built-in wallpaper that simply shows a static image. |
| */ |
| @SuppressWarnings({"UnusedDeclaration"}) |
| public class ImageWallpaper extends WallpaperService { |
| private static final String TAG = ImageWallpaper.class.getSimpleName(); |
| // We delayed destroy render context that subsequent render requests have chance to cancel it. |
| // This is to avoid destroying then recreating render context in a very short time. |
| private static final int DELAY_FINISH_RENDERING = 1000; |
| 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 boolean mPagesComputed = false; |
| private HandlerThread mWorker; |
| // scaled down version |
| private Bitmap mMiniBitmap; |
| private final FeatureFlags mFeatureFlags; |
| |
| // used in canvasEngine to load/unload the bitmap and extract the colors |
| @Background |
| private final DelayableExecutor mBackgroundExecutor; |
| private static final int DELAY_UNLOAD_BITMAP = 2000; |
| |
| @Main |
| private final Executor mMainExecutor; |
| |
| @Inject |
| public ImageWallpaper(FeatureFlags featureFlags, |
| @Background DelayableExecutor backgroundExecutor, |
| @Main Executor mainExecutor) { |
| super(); |
| mFeatureFlags = featureFlags; |
| mBackgroundExecutor = backgroundExecutor; |
| mMainExecutor = mainExecutor; |
| } |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| mWorker = new HandlerThread(TAG); |
| mWorker.start(); |
| } |
| |
| @Override |
| public Engine onCreateEngine() { |
| return mFeatureFlags.isEnabled(USE_CANVAS_RENDERER) ? new CanvasEngine() : new GLEngine(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| mWorker.quitSafely(); |
| mWorker = null; |
| mMiniBitmap = null; |
| } |
| |
| class GLEngine extends Engine implements DisplayListener { |
| // Surface is rejected if size below a threshold on some devices (ie. 8px on elfin) |
| // set min to 64 px (CTS covers this), please refer to ag/4867989 for detail. |
| @VisibleForTesting |
| static final int MIN_SURFACE_WIDTH = 128; |
| @VisibleForTesting |
| static final int MIN_SURFACE_HEIGHT = 128; |
| |
| private ImageWallpaperRenderer mRenderer; |
| private EglHelper mEglHelper; |
| private final Runnable mFinishRenderingTask = this::finishRendering; |
| private boolean mNeedRedraw; |
| |
| private boolean mDisplaySizeValid = false; |
| private int mDisplayWidth = 1; |
| private int mDisplayHeight = 1; |
| |
| private int mImgWidth = 1; |
| private int mImgHeight = 1; |
| |
| GLEngine() { } |
| |
| @VisibleForTesting |
| GLEngine(Handler handler) { |
| super(SystemClock::elapsedRealtime, handler); |
| } |
| |
| @Override |
| public void onCreate(SurfaceHolder surfaceHolder) { |
| Trace.beginSection("ImageWallpaper.Engine#onCreate"); |
| mEglHelper = getEglHelperInstance(); |
| // Deferred init renderer because we need to get wallpaper by display context. |
| mRenderer = getRendererInstance(); |
| setFixedSizeAllowed(true); |
| updateSurfaceSize(); |
| setShowForAllUsers(true); |
| |
| mRenderer.setOnBitmapChanged(b -> { |
| mLocalColorsToAdd.addAll(mColorAreas); |
| if (mLocalColorsToAdd.size() > 0) { |
| updateMiniBitmapAndNotify(b); |
| } |
| }); |
| getDisplayContext().getSystemService(DisplayManager.class) |
| .registerDisplayListener(this, mWorker.getThreadHandler()); |
| Trace.endSection(); |
| } |
| |
| @Override |
| public void onDisplayAdded(int displayId) { } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) { } |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| if (displayId == getDisplayContext().getDisplayId()) { |
| mDisplaySizeValid = false; |
| } |
| } |
| |
| EglHelper getEglHelperInstance() { |
| return new EglHelper(); |
| } |
| |
| ImageWallpaperRenderer getRendererInstance() { |
| return new ImageWallpaperRenderer(getDisplayContext()); |
| } |
| |
| @Override |
| public void onOffsetsChanged(float xOffset, float yOffset, |
| float xOffsetStep, float yOffsetStep, |
| int xPixelOffset, int yPixelOffset) { |
| final int pages; |
| if (xOffsetStep > 0 && xOffsetStep <= 1) { |
| pages = (int) Math.round(1 / xOffsetStep) + 1; |
| } else { |
| pages = 1; |
| } |
| if (pages == mPages) return; |
| mPages = pages; |
| if (mMiniBitmap == null || mMiniBitmap.isRecycled()) return; |
| mWorker.getThreadHandler().post(() -> |
| computeAndNotifyLocalColors(new ArrayList<>(mColorAreas), mMiniBitmap)); |
| } |
| |
| private void updateMiniBitmapAndNotify(Bitmap b) { |
| if (b == null) return; |
| int size = Math.min(b.getWidth(), b.getHeight()); |
| float scale = 1.0f; |
| if (size > MIN_SURFACE_WIDTH) { |
| scale = (float) MIN_SURFACE_WIDTH / (float) size; |
| } |
| mImgHeight = b.getHeight(); |
| mImgWidth = b.getWidth(); |
| mMiniBitmap = Bitmap.createScaledBitmap(b, (int) Math.max(scale * b.getWidth(), 1), |
| (int) Math.max(scale * b.getHeight(), 1), false); |
| computeAndNotifyLocalColors(mLocalColorsToAdd, mMiniBitmap); |
| mLocalColorsToAdd.clear(); |
| } |
| |
| private void updateSurfaceSize() { |
| Trace.beginSection("ImageWallpaper#updateSurfaceSize"); |
| SurfaceHolder holder = getSurfaceHolder(); |
| Size frameSize = mRenderer.reportSurfaceSize(); |
| int width = Math.max(MIN_SURFACE_WIDTH, frameSize.getWidth()); |
| int height = Math.max(MIN_SURFACE_HEIGHT, frameSize.getHeight()); |
| holder.setFixedSize(width, height); |
| Trace.endSection(); |
| } |
| |
| @Override |
| public boolean shouldZoomOutWallpaper() { |
| return true; |
| } |
| |
| @Override |
| public boolean shouldWaitForEngineShown() { |
| return true; |
| } |
| |
| @Override |
| public void onDestroy() { |
| getDisplayContext().getSystemService(DisplayManager.class) |
| .unregisterDisplayListener(this); |
| mMiniBitmap = null; |
| mWorker.getThreadHandler().post(() -> { |
| mRenderer.finish(); |
| mRenderer = null; |
| mEglHelper.finish(); |
| mEglHelper = null; |
| }); |
| } |
| |
| @Override |
| public boolean supportsLocalColorExtraction() { |
| return true; |
| } |
| |
| @Override |
| public void addLocalColorsAreas(@NonNull List<RectF> regions) { |
| mWorker.getThreadHandler().post(() -> { |
| if (mColorAreas.size() + mLocalColorsToAdd.size() == 0) { |
| setOffsetNotificationsEnabled(true); |
| } |
| Bitmap bitmap = mMiniBitmap; |
| if (bitmap == null) { |
| mLocalColorsToAdd.addAll(regions); |
| if (mRenderer != null) mRenderer.use(this::updateMiniBitmapAndNotify); |
| } else { |
| computeAndNotifyLocalColors(regions, bitmap); |
| } |
| }); |
| } |
| |
| private void computeAndNotifyLocalColors(@NonNull List<RectF> regions, Bitmap b) { |
| List<WallpaperColors> colors = getLocalWallpaperColors(regions, b); |
| mColorAreas.addAll(regions); |
| try { |
| notifyLocalColorsChanged(regions, colors); |
| } catch (RuntimeException e) { |
| Log.e(TAG, e.getMessage(), e); |
| } |
| } |
| |
| @Override |
| public void removeLocalColorsAreas(@NonNull List<RectF> regions) { |
| mWorker.getThreadHandler().post(() -> { |
| mColorAreas.removeAll(regions); |
| mLocalColorsToAdd.removeAll(regions); |
| if (mColorAreas.size() + mLocalColorsToAdd.size() == 0) { |
| setOffsetNotificationsEnabled(false); |
| } |
| }); |
| } |
| |
| /** |
| * Transform the logical coordinates into wallpaper coordinates. |
| * |
| * Logical coordinates are organised such that the various pages are non-overlapping. So, |
| * if there are n pages, the first page will have its X coordinate on the range [0-1/n]. |
| * |
| * The real pages are overlapping. If the Wallpaper are a width Ww and the screen a width |
| * Ws, the relative width of a page Wr is Ws/Ww. This does not change if the number of |
| * pages increase. |
| * If there are n pages, the page k starts at the offset k * (1 - Wr) / (n - 1), as the |
| * last page is at position (1-Wr) and the others are regularly spread on the range [0- |
| * (1-Wr)]. |
| */ |
| private RectF pageToImgRect(RectF area) { |
| if (!mDisplaySizeValid) { |
| Rect window = getDisplayContext() |
| .getSystemService(WindowManager.class) |
| .getCurrentWindowMetrics() |
| .getBounds(); |
| mDisplayWidth = window.width(); |
| mDisplayHeight = window.height(); |
| mDisplaySizeValid = true; |
| } |
| |
| // Width of a page for the caller of this API. |
| float virtualPageWidth = 1f / (float) mPages; |
| float leftPosOnPage = (area.left % virtualPageWidth) / virtualPageWidth; |
| float rightPosOnPage = (area.right % virtualPageWidth) / virtualPageWidth; |
| int currentPage = (int) Math.floor(area.centerX() / virtualPageWidth); |
| |
| RectF imgArea = new RectF(); |
| |
| if (mImgWidth == 0 || mImgHeight == 0 || mDisplayWidth <= 0 || mDisplayHeight <= 0) { |
| return imgArea; |
| } |
| |
| imgArea.bottom = area.bottom; |
| imgArea.top = area.top; |
| |
| float imageScale = Math.min(((float) mImgHeight) / mDisplayHeight, 1); |
| float mappedScreenWidth = mDisplayWidth * imageScale; |
| float pageWidth = Math.min(1.0f, |
| mImgWidth > 0 ? mappedScreenWidth / (float) mImgWidth : 1.f); |
| float pageOffset = (1 - pageWidth) / (float) (mPages - 1); |
| |
| imgArea.left = MathUtils.constrain( |
| leftPosOnPage * pageWidth + currentPage * pageOffset, 0, 1); |
| imgArea.right = MathUtils.constrain( |
| rightPosOnPage * pageWidth + currentPage * pageOffset, 0, 1); |
| if (imgArea.left > imgArea.right) { |
| // take full page |
| imgArea.left = 0; |
| imgArea.right = 1; |
| } |
| return imgArea; |
| } |
| |
| private List<WallpaperColors> getLocalWallpaperColors(@NonNull List<RectF> areas, |
| Bitmap b) { |
| List<WallpaperColors> colors = new ArrayList<>(areas.size()); |
| for (int i = 0; i < areas.size(); i++) { |
| RectF area = pageToImgRect(areas.get(i)); |
| if (area == null || !LOCAL_COLOR_BOUNDS.contains(area)) { |
| colors.add(null); |
| continue; |
| } |
| Rect subImage = new Rect( |
| (int) Math.floor(area.left * b.getWidth()), |
| (int) Math.floor(area.top * b.getHeight()), |
| (int) Math.ceil(area.right * b.getWidth()), |
| (int) Math.ceil(area.bottom * b.getHeight())); |
| if (subImage.isEmpty()) { |
| // Do not notify client. treat it as too small to sample |
| colors.add(null); |
| continue; |
| } |
| Bitmap colorImg = Bitmap.createBitmap(b, |
| subImage.left, subImage.top, subImage.width(), subImage.height()); |
| WallpaperColors color = WallpaperColors.fromBitmap(colorImg); |
| colors.add(color); |
| } |
| return colors; |
| } |
| |
| @Override |
| public void onSurfaceCreated(SurfaceHolder holder) { |
| if (mWorker == null) return; |
| mWorker.getThreadHandler().post(() -> { |
| Trace.beginSection("ImageWallpaper#onSurfaceCreated"); |
| mEglHelper.init(holder, needSupportWideColorGamut()); |
| mRenderer.onSurfaceCreated(); |
| Trace.endSection(); |
| }); |
| } |
| |
| @Override |
| public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { |
| if (mWorker == null) return; |
| mWorker.getThreadHandler().post(() -> mRenderer.onSurfaceChanged(width, height)); |
| } |
| |
| @Override |
| public void onSurfaceRedrawNeeded(SurfaceHolder holder) { |
| if (mWorker == null) return; |
| mWorker.getThreadHandler().post(this::drawFrame); |
| } |
| |
| private void drawFrame() { |
| Trace.beginSection("ImageWallpaper#drawFrame"); |
| preRender(); |
| requestRender(); |
| postRender(); |
| Trace.endSection(); |
| } |
| |
| public void preRender() { |
| // This method should only be invoked from worker thread. |
| Trace.beginSection("ImageWallpaper#preRender"); |
| preRenderInternal(); |
| Trace.endSection(); |
| } |
| |
| private void preRenderInternal() { |
| boolean contextRecreated = false; |
| Rect frame = getSurfaceHolder().getSurfaceFrame(); |
| cancelFinishRenderingTask(); |
| |
| // Check if we need to recreate egl context. |
| if (!mEglHelper.hasEglContext()) { |
| mEglHelper.destroyEglSurface(); |
| if (!mEglHelper.createEglContext()) { |
| Log.w(TAG, "recreate egl context failed!"); |
| } else { |
| contextRecreated = true; |
| } |
| } |
| |
| // Check if we need to recreate egl surface. |
| if (mEglHelper.hasEglContext() && !mEglHelper.hasEglSurface()) { |
| if (!mEglHelper.createEglSurface(getSurfaceHolder(), needSupportWideColorGamut())) { |
| Log.w(TAG, "recreate egl surface failed!"); |
| } |
| } |
| |
| // If we recreate egl context, notify renderer to setup again. |
| if (mEglHelper.hasEglContext() && mEglHelper.hasEglSurface() && contextRecreated) { |
| mRenderer.onSurfaceCreated(); |
| mRenderer.onSurfaceChanged(frame.width(), frame.height()); |
| } |
| } |
| |
| public void requestRender() { |
| // This method should only be invoked from worker thread. |
| Trace.beginSection("ImageWallpaper#requestRender"); |
| requestRenderInternal(); |
| Trace.endSection(); |
| } |
| |
| private void requestRenderInternal() { |
| Rect frame = getSurfaceHolder().getSurfaceFrame(); |
| boolean readyToRender = mEglHelper.hasEglContext() && mEglHelper.hasEglSurface() |
| && frame.width() > 0 && frame.height() > 0; |
| |
| if (readyToRender) { |
| mRenderer.onDrawFrame(); |
| if (!mEglHelper.swapBuffer()) { |
| Log.e(TAG, "drawFrame failed!"); |
| } |
| } else { |
| Log.e(TAG, "requestRender: not ready, has context=" + mEglHelper.hasEglContext() |
| + ", has surface=" + mEglHelper.hasEglSurface() |
| + ", frame=" + frame); |
| } |
| } |
| |
| public void postRender() { |
| // This method should only be invoked from worker thread. |
| scheduleFinishRendering(); |
| reportEngineShown(false /* waitForEngineShown */); |
| } |
| |
| private void cancelFinishRenderingTask() { |
| if (mWorker == null) return; |
| mWorker.getThreadHandler().removeCallbacks(mFinishRenderingTask); |
| } |
| |
| private void scheduleFinishRendering() { |
| if (mWorker == null) return; |
| cancelFinishRenderingTask(); |
| mWorker.getThreadHandler().postDelayed(mFinishRenderingTask, DELAY_FINISH_RENDERING); |
| } |
| |
| private void finishRendering() { |
| Trace.beginSection("ImageWallpaper#finishRendering"); |
| if (mEglHelper != null) { |
| mEglHelper.destroyEglSurface(); |
| mEglHelper.destroyEglContext(); |
| } |
| Trace.endSection(); |
| } |
| |
| private boolean needSupportWideColorGamut() { |
| return mRenderer.isWcgContent(); |
| } |
| |
| @Override |
| 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); |
| out.print(prefix); out.print("valid surface="); |
| out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null |
| ? getSurfaceHolder().getSurface().isValid() |
| : "null"); |
| |
| out.print(prefix); out.print("surface frame="); |
| out.println(getSurfaceHolder() != null ? getSurfaceHolder().getSurfaceFrame() : "null"); |
| |
| mEglHelper.dump(prefix, fd, out, args); |
| mRenderer.dump(prefix, fd, out, args); |
| } |
| } |
| |
| |
| class CanvasEngine extends WallpaperService.Engine implements DisplayListener { |
| private WallpaperManager mWallpaperManager; |
| private final WallpaperColorExtractor mWallpaperColorExtractor; |
| private SurfaceHolder mSurfaceHolder; |
| @VisibleForTesting |
| static final int MIN_SURFACE_WIDTH = 128; |
| @VisibleForTesting |
| static final int MIN_SURFACE_HEIGHT = 128; |
| private Bitmap mBitmap; |
| private boolean mWideColorGamut = false; |
| |
| /* |
| * Counter to unload the bitmap as soon as possible. |
| * Before any bitmap operation, this is incremented. |
| * After an operation completion, this is decremented (synchronously), |
| * and if the count is 0, unload the bitmap |
| */ |
| private int mBitmapUsages = 0; |
| private final Object mLock = new Object(); |
| |
| CanvasEngine() { |
| super(); |
| setFixedSizeAllowed(true); |
| setShowForAllUsers(true); |
| mWallpaperColorExtractor = new WallpaperColorExtractor( |
| mBackgroundExecutor, |
| new WallpaperColorExtractor.WallpaperColorExtractorCallback() { |
| @Override |
| public void onColorsProcessed(List<RectF> regions, |
| List<WallpaperColors> colors) { |
| CanvasEngine.this.onColorsProcessed(regions, colors); |
| } |
| |
| @Override |
| public void onMiniBitmapUpdated() { |
| CanvasEngine.this.onMiniBitmapUpdated(); |
| } |
| |
| @Override |
| public void onActivated() { |
| setOffsetNotificationsEnabled(true); |
| } |
| |
| @Override |
| public void onDeactivated() { |
| setOffsetNotificationsEnabled(false); |
| } |
| }); |
| |
| // if the number of pages is already computed, transmit it to the color extractor |
| if (mPagesComputed) { |
| mWallpaperColorExtractor.onPageChanged(mPages); |
| } |
| } |
| |
| @Override |
| public void onCreate(SurfaceHolder surfaceHolder) { |
| Trace.beginSection("ImageWallpaper.CanvasEngine#onCreate"); |
| if (DEBUG) { |
| Log.d(TAG, "onCreate"); |
| } |
| mWallpaperManager = getDisplayContext().getSystemService(WallpaperManager.class); |
| mSurfaceHolder = surfaceHolder; |
| Rect dimensions = mWallpaperManager.peekBitmapDimensions(); |
| int width = Math.max(MIN_SURFACE_WIDTH, dimensions.width()); |
| int height = Math.max(MIN_SURFACE_HEIGHT, dimensions.height()); |
| mSurfaceHolder.setFixedSize(width, height); |
| |
| getDisplayContext().getSystemService(DisplayManager.class) |
| .registerDisplayListener(this, null); |
| getDisplaySizeAndUpdateColorExtractor(); |
| Trace.endSection(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| getDisplayContext().getSystemService(DisplayManager.class) |
| .unregisterDisplayListener(this); |
| mWallpaperColorExtractor.cleanUp(); |
| unloadBitmap(); |
| } |
| |
| @Override |
| public boolean shouldZoomOutWallpaper() { |
| return true; |
| } |
| |
| @Override |
| public boolean shouldWaitForEngineShown() { |
| return true; |
| } |
| |
| @Override |
| public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { |
| if (DEBUG) { |
| Log.d(TAG, "onSurfaceChanged: width=" + width + ", height=" + height); |
| } |
| } |
| |
| @Override |
| public void onSurfaceDestroyed(SurfaceHolder holder) { |
| if (DEBUG) { |
| Log.i(TAG, "onSurfaceDestroyed"); |
| } |
| mSurfaceHolder = null; |
| } |
| |
| @Override |
| public void onSurfaceCreated(SurfaceHolder holder) { |
| if (DEBUG) { |
| Log.i(TAG, "onSurfaceCreated"); |
| } |
| } |
| |
| @Override |
| public void onSurfaceRedrawNeeded(SurfaceHolder holder) { |
| if (DEBUG) { |
| Log.d(TAG, "onSurfaceRedrawNeeded"); |
| } |
| drawFrame(); |
| } |
| |
| private void drawFrame() { |
| mBackgroundExecutor.execute(this::drawFrameSynchronized); |
| } |
| |
| private void drawFrameSynchronized() { |
| synchronized (mLock) { |
| drawFrameInternal(); |
| } |
| } |
| |
| private void drawFrameInternal() { |
| if (mSurfaceHolder == null) { |
| Log.e(TAG, "attempt to draw a frame without a valid surface"); |
| return; |
| } |
| |
| // load the wallpaper if not already done |
| if (!isBitmapLoaded()) { |
| loadWallpaperAndDrawFrameInternal(); |
| } else { |
| mBitmapUsages++; |
| |
| // drawing is done on the main thread |
| mMainExecutor.execute(() -> { |
| drawFrameOnCanvas(mBitmap); |
| reportEngineShown(false); |
| unloadBitmapIfNotUsed(); |
| }); |
| } |
| } |
| |
| @VisibleForTesting |
| void drawFrameOnCanvas(Bitmap bitmap) { |
| Trace.beginSection("ImageWallpaper.CanvasEngine#drawFrame"); |
| Surface surface = mSurfaceHolder.getSurface(); |
| Canvas canvas = mWideColorGamut |
| ? surface.lockHardwareWideColorGamutCanvas() |
| : surface.lockHardwareCanvas(); |
| if (canvas != null) { |
| Rect dest = mSurfaceHolder.getSurfaceFrame(); |
| try { |
| canvas.drawBitmap(bitmap, null, dest, null); |
| } finally { |
| surface.unlockCanvasAndPost(canvas); |
| } |
| } |
| Trace.endSection(); |
| } |
| |
| @VisibleForTesting |
| boolean isBitmapLoaded() { |
| return mBitmap != null && !mBitmap.isRecycled(); |
| } |
| |
| private void unloadBitmapIfNotUsed() { |
| mBackgroundExecutor.execute(this::unloadBitmapIfNotUsedSynchronized); |
| } |
| |
| private void unloadBitmapIfNotUsedSynchronized() { |
| synchronized (mLock) { |
| mBitmapUsages -= 1; |
| if (mBitmapUsages <= 0) { |
| mBitmapUsages = 0; |
| unloadBitmapInternal(); |
| } |
| } |
| } |
| |
| private void unloadBitmap() { |
| mBackgroundExecutor.execute(this::unloadBitmapSynchronized); |
| } |
| |
| private void unloadBitmapSynchronized() { |
| synchronized (mLock) { |
| mBitmapUsages = 0; |
| unloadBitmapInternal(); |
| } |
| } |
| |
| private void unloadBitmapInternal() { |
| Trace.beginSection("ImageWallpaper.CanvasEngine#unloadBitmap"); |
| if (mBitmap != null) { |
| mBitmap.recycle(); |
| } |
| mBitmap = null; |
| |
| final Surface surface = getSurfaceHolder().getSurface(); |
| surface.hwuiDestroy(); |
| mWallpaperManager.forgetLoadedWallpaper(); |
| Trace.endSection(); |
| } |
| |
| private void loadWallpaperAndDrawFrameInternal() { |
| Trace.beginSection("ImageWallpaper.CanvasEngine#loadWallpaper"); |
| boolean loadSuccess = false; |
| Bitmap bitmap; |
| try { |
| bitmap = mWallpaperManager.getBitmap(false); |
| if (bitmap != null |
| && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) { |
| throw new RuntimeException("Wallpaper is too large to draw!"); |
| } |
| } catch (RuntimeException | OutOfMemoryError exception) { |
| |
| // 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(WallpaperManager.FLAG_SYSTEM); |
| } catch (IOException ex) { |
| // now we're really screwed. |
| Log.w(TAG, "Unable reset to default wallpaper!", ex); |
| } |
| |
| try { |
| bitmap = mWallpaperManager.getBitmap(false); |
| } catch (RuntimeException | OutOfMemoryError e) { |
| Log.w(TAG, "Unable to load default wallpaper!", e); |
| bitmap = null; |
| } |
| } |
| |
| if (bitmap == null) { |
| Log.w(TAG, "Could not load bitmap"); |
| } else if (bitmap.isRecycled()) { |
| Log.e(TAG, "Attempt to load a recycled bitmap"); |
| } else if (mBitmap == bitmap) { |
| Log.e(TAG, "Loaded a bitmap that was already loaded"); |
| } else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) { |
| Log.e(TAG, "Attempt to load an invalid wallpaper of length " |
| + bitmap.getWidth() + "x" + bitmap.getHeight()); |
| } else { |
| // at this point, loading is done correctly. |
| loadSuccess = true; |
| // recycle the previously loaded bitmap |
| if (mBitmap != null) { |
| mBitmap.recycle(); |
| } |
| mBitmap = bitmap; |
| mWideColorGamut = mWallpaperManager.wallpaperSupportsWcg( |
| WallpaperManager.FLAG_SYSTEM); |
| |
| // +2 usages for the color extraction and the delayed unload. |
| mBitmapUsages += 2; |
| recomputeColorExtractorMiniBitmap(); |
| drawFrameInternal(); |
| |
| /* |
| * after loading, the bitmap will be unloaded after all these conditions: |
| * - the frame is redrawn |
| * - the mini bitmap from color extractor is recomputed |
| * - the DELAY_UNLOAD_BITMAP has passed |
| */ |
| mBackgroundExecutor.executeDelayed( |
| this::unloadBitmapIfNotUsedSynchronized, DELAY_UNLOAD_BITMAP); |
| } |
| // even if the bitmap cannot be loaded, call reportEngineShown |
| if (!loadSuccess) reportEngineShown(false); |
| Trace.endSection(); |
| } |
| |
| private void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { |
| try { |
| notifyLocalColorsChanged(regions, colors); |
| } catch (RuntimeException e) { |
| Log.e(TAG, e.getMessage(), e); |
| } |
| } |
| |
| @VisibleForTesting |
| void recomputeColorExtractorMiniBitmap() { |
| mWallpaperColorExtractor.onBitmapChanged(mBitmap); |
| } |
| |
| @VisibleForTesting |
| void onMiniBitmapUpdated() { |
| unloadBitmapIfNotUsed(); |
| } |
| |
| @Override |
| public boolean supportsLocalColorExtraction() { |
| return true; |
| } |
| |
| @Override |
| public void addLocalColorsAreas(@NonNull List<RectF> regions) { |
| // this call will activate the offset notifications |
| // if no colors were being processed before |
| mWallpaperColorExtractor.addLocalColorsAreas(regions); |
| } |
| |
| @Override |
| public void removeLocalColorsAreas(@NonNull List<RectF> regions) { |
| // this call will deactivate the offset notifications |
| // if we are no longer processing colors |
| mWallpaperColorExtractor.removeLocalColorAreas(regions); |
| } |
| |
| @Override |
| public void onOffsetsChanged(float xOffset, float yOffset, |
| float xOffsetStep, float yOffsetStep, |
| int xPixelOffset, int yPixelOffset) { |
| final int pages; |
| if (xOffsetStep > 0 && xOffsetStep <= 1) { |
| pages = Math.round(1 / xOffsetStep) + 1; |
| } else { |
| pages = 1; |
| } |
| if (pages != mPages || !mPagesComputed) { |
| mPages = pages; |
| mPagesComputed = true; |
| mWallpaperColorExtractor.onPageChanged(mPages); |
| } |
| } |
| |
| @Override |
| public void onDisplayAdded(int displayId) { |
| |
| } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) { |
| |
| } |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| // changes the display in the color extractor |
| // the new display dimensions will be used in the next color computation |
| if (displayId == getDisplayContext().getDisplayId()) { |
| getDisplaySizeAndUpdateColorExtractor(); |
| } |
| } |
| |
| private void getDisplaySizeAndUpdateColorExtractor() { |
| Rect window = getDisplayContext() |
| .getSystemService(WindowManager.class) |
| .getCurrentWindowMetrics() |
| .getBounds(); |
| mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height()); |
| } |
| |
| |
| @Override |
| 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); |
| out.print(prefix); out.print("valid surface="); |
| out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null |
| ? getSurfaceHolder().getSurface().isValid() |
| : "null"); |
| |
| out.print(prefix); out.print("surface frame="); |
| out.println(getSurfaceHolder() != null ? getSurfaceHolder().getSurfaceFrame() : "null"); |
| |
| out.print(prefix); out.print("bitmap="); |
| out.println(mBitmap == null ? "null" |
| : mBitmap.isRecycled() ? "recycled" |
| : mBitmap.getWidth() + "x" + mBitmap.getHeight()); |
| |
| mWallpaperColorExtractor.dump(prefix, fd, out, args); |
| } |
| } |
| } |