1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.wallpapers; 18 19 import android.app.WallpaperColors; 20 import android.app.WallpaperManager; 21 import android.graphics.Bitmap; 22 import android.graphics.Canvas; 23 import android.graphics.RecordingCanvas; 24 import android.graphics.Rect; 25 import android.graphics.RectF; 26 import android.hardware.display.DisplayManager; 27 import android.hardware.display.DisplayManager.DisplayListener; 28 import android.os.Trace; 29 import android.service.wallpaper.WallpaperService; 30 import android.util.Log; 31 import android.view.Surface; 32 import android.view.SurfaceHolder; 33 import android.view.WindowManager; 34 35 import androidx.annotation.NonNull; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.systemui.dagger.qualifiers.LongRunning; 39 import com.android.systemui.settings.UserTracker; 40 import com.android.systemui.util.concurrency.DelayableExecutor; 41 42 import java.io.FileDescriptor; 43 import java.io.PrintWriter; 44 import java.util.List; 45 46 import javax.inject.Inject; 47 48 /** 49 * Default built-in wallpaper that simply shows a static image. 50 */ 51 @SuppressWarnings({"UnusedDeclaration"}) 52 public class ImageWallpaper extends WallpaperService { 53 54 private static final String TAG = ImageWallpaper.class.getSimpleName(); 55 private static final boolean DEBUG = false; 56 57 // keep track of the number of pages of the launcher for local color extraction purposes 58 private volatile int mPages = 1; 59 private boolean mPagesComputed = false; 60 61 private final UserTracker mUserTracker; 62 63 // used for most tasks (call canvas.drawBitmap, load/unload the bitmap) 64 @LongRunning 65 private final DelayableExecutor mLongExecutor; 66 67 // wait at least this duration before unloading the bitmap 68 private static final int DELAY_UNLOAD_BITMAP = 2000; 69 70 @Inject ImageWallpaper(@ongRunning DelayableExecutor longExecutor, UserTracker userTracker)71 public ImageWallpaper(@LongRunning DelayableExecutor longExecutor, UserTracker userTracker) { 72 super(); 73 mLongExecutor = longExecutor; 74 mUserTracker = userTracker; 75 } 76 77 @Override onCreateEngine()78 public Engine onCreateEngine() { 79 return new CanvasEngine(); 80 } 81 82 class CanvasEngine extends WallpaperService.Engine implements DisplayListener { 83 private WallpaperManager mWallpaperManager; 84 private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor; 85 private SurfaceHolder mSurfaceHolder; 86 @VisibleForTesting 87 static final int MIN_SURFACE_WIDTH = 128; 88 @VisibleForTesting 89 static final int MIN_SURFACE_HEIGHT = 128; 90 private Bitmap mBitmap; 91 private boolean mWideColorGamut = false; 92 93 /* 94 * Counter to unload the bitmap as soon as possible. 95 * Before any bitmap operation, this is incremented. 96 * After an operation completion, this is decremented (synchronously), 97 * and if the count is 0, unload the bitmap 98 */ 99 private int mBitmapUsages = 0; 100 private final Object mLock = new Object(); 101 CanvasEngine()102 CanvasEngine() { 103 super(); 104 setFixedSizeAllowed(true); 105 setShowForAllUsers(true); 106 mWallpaperLocalColorExtractor = new WallpaperLocalColorExtractor( 107 mLongExecutor, 108 new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { 109 @Override 110 public void onColorsProcessed(List<RectF> regions, 111 List<WallpaperColors> colors) { 112 CanvasEngine.this.onColorsProcessed(regions, colors); 113 } 114 115 @Override 116 public void onMiniBitmapUpdated() { 117 CanvasEngine.this.onMiniBitmapUpdated(); 118 } 119 120 @Override 121 public void onActivated() { 122 setOffsetNotificationsEnabled(true); 123 } 124 125 @Override 126 public void onDeactivated() { 127 setOffsetNotificationsEnabled(false); 128 } 129 }); 130 131 // if the number of pages is already computed, transmit it to the color extractor 132 if (mPagesComputed) { 133 mWallpaperLocalColorExtractor.onPageChanged(mPages); 134 } 135 } 136 137 @Override onCreate(SurfaceHolder surfaceHolder)138 public void onCreate(SurfaceHolder surfaceHolder) { 139 Trace.beginSection("ImageWallpaper.CanvasEngine#onCreate"); 140 if (DEBUG) { 141 Log.d(TAG, "onCreate"); 142 } 143 mWallpaperManager = getDisplayContext().getSystemService(WallpaperManager.class); 144 mSurfaceHolder = surfaceHolder; 145 Rect dimensions = mWallpaperManager.peekBitmapDimensions(); 146 int width = Math.max(MIN_SURFACE_WIDTH, dimensions.width()); 147 int height = Math.max(MIN_SURFACE_HEIGHT, dimensions.height()); 148 mSurfaceHolder.setFixedSize(width, height); 149 150 getDisplayContext().getSystemService(DisplayManager.class) 151 .registerDisplayListener(this, null); 152 getDisplaySizeAndUpdateColorExtractor(); 153 Trace.endSection(); 154 } 155 156 @Override onDestroy()157 public void onDestroy() { 158 getDisplayContext().getSystemService(DisplayManager.class) 159 .unregisterDisplayListener(this); 160 mWallpaperLocalColorExtractor.cleanUp(); 161 } 162 163 @Override shouldZoomOutWallpaper()164 public boolean shouldZoomOutWallpaper() { 165 return true; 166 } 167 168 @Override shouldWaitForEngineShown()169 public boolean shouldWaitForEngineShown() { 170 return true; 171 } 172 173 @Override onSurfaceChanged(SurfaceHolder holder, int format, int width, int height)174 public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { 175 if (DEBUG) { 176 Log.d(TAG, "onSurfaceChanged: width=" + width + ", height=" + height); 177 } 178 } 179 180 @Override onSurfaceDestroyed(SurfaceHolder holder)181 public void onSurfaceDestroyed(SurfaceHolder holder) { 182 if (DEBUG) { 183 Log.i(TAG, "onSurfaceDestroyed"); 184 } 185 mSurfaceHolder = null; 186 } 187 188 @Override onSurfaceCreated(SurfaceHolder holder)189 public void onSurfaceCreated(SurfaceHolder holder) { 190 if (DEBUG) { 191 Log.i(TAG, "onSurfaceCreated"); 192 } 193 } 194 195 @Override onSurfaceRedrawNeeded(SurfaceHolder holder)196 public void onSurfaceRedrawNeeded(SurfaceHolder holder) { 197 if (DEBUG) { 198 Log.d(TAG, "onSurfaceRedrawNeeded"); 199 } 200 drawFrame(); 201 } 202 drawFrame()203 private void drawFrame() { 204 mLongExecutor.execute(this::drawFrameSynchronized); 205 } 206 drawFrameSynchronized()207 private void drawFrameSynchronized() { 208 synchronized (mLock) { 209 drawFrameInternal(); 210 } 211 } 212 drawFrameInternal()213 private void drawFrameInternal() { 214 if (mSurfaceHolder == null) { 215 Log.e(TAG, "attempt to draw a frame without a valid surface"); 216 return; 217 } 218 219 // load the wallpaper if not already done 220 if (!isBitmapLoaded()) { 221 loadWallpaperAndDrawFrameInternal(); 222 } else { 223 mBitmapUsages++; 224 drawFrameOnCanvas(mBitmap); 225 reportEngineShown(false); 226 unloadBitmapIfNotUsedInternal(); 227 } 228 } 229 230 @VisibleForTesting drawFrameOnCanvas(Bitmap bitmap)231 void drawFrameOnCanvas(Bitmap bitmap) { 232 Trace.beginSection("ImageWallpaper.CanvasEngine#drawFrame"); 233 Surface surface = mSurfaceHolder.getSurface(); 234 Canvas canvas = null; 235 try { 236 canvas = mWideColorGamut 237 ? surface.lockHardwareWideColorGamutCanvas() 238 : surface.lockHardwareCanvas(); 239 } catch (IllegalStateException e) { 240 Log.w(TAG, "Unable to lock canvas", e); 241 } 242 if (canvas != null) { 243 Rect dest = mSurfaceHolder.getSurfaceFrame(); 244 try { 245 canvas.drawBitmap(bitmap, null, dest, null); 246 } finally { 247 surface.unlockCanvasAndPost(canvas); 248 } 249 } 250 Trace.endSection(); 251 } 252 253 @VisibleForTesting isBitmapLoaded()254 boolean isBitmapLoaded() { 255 return mBitmap != null && !mBitmap.isRecycled(); 256 } 257 unloadBitmapIfNotUsed()258 private void unloadBitmapIfNotUsed() { 259 mLongExecutor.execute(this::unloadBitmapIfNotUsedSynchronized); 260 } 261 unloadBitmapIfNotUsedSynchronized()262 private void unloadBitmapIfNotUsedSynchronized() { 263 synchronized (mLock) { 264 unloadBitmapIfNotUsedInternal(); 265 } 266 } 267 unloadBitmapIfNotUsedInternal()268 private void unloadBitmapIfNotUsedInternal() { 269 mBitmapUsages -= 1; 270 if (mBitmapUsages <= 0) { 271 mBitmapUsages = 0; 272 unloadBitmapInternal(); 273 } 274 } 275 unloadBitmapInternal()276 private void unloadBitmapInternal() { 277 Trace.beginSection("ImageWallpaper.CanvasEngine#unloadBitmap"); 278 if (mBitmap != null) { 279 mBitmap.recycle(); 280 } 281 mBitmap = null; 282 283 final Surface surface = getSurfaceHolder().getSurface(); 284 surface.hwuiDestroy(); 285 mWallpaperManager.forgetLoadedWallpaper(); 286 Trace.endSection(); 287 } 288 loadWallpaperAndDrawFrameInternal()289 private void loadWallpaperAndDrawFrameInternal() { 290 Trace.beginSection("ImageWallpaper.CanvasEngine#loadWallpaper"); 291 boolean loadSuccess = false; 292 Bitmap bitmap; 293 try { 294 bitmap = mWallpaperManager.getBitmapAsUser(mUserTracker.getUserId(), false); 295 if (bitmap != null 296 && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) { 297 throw new RuntimeException("Wallpaper is too large to draw!"); 298 } 299 } catch (RuntimeException | OutOfMemoryError exception) { 300 301 // Note that if we do fail at this, and the default wallpaper can't 302 // be loaded, we will go into a cycle. Don't do a build where the 303 // default wallpaper can't be loaded. 304 Log.w(TAG, "Unable to load wallpaper!", exception); 305 mWallpaperManager.clearWallpaper( 306 WallpaperManager.FLAG_SYSTEM, mUserTracker.getUserId()); 307 try { 308 bitmap = mWallpaperManager.getBitmapAsUser(mUserTracker.getUserId(), false); 309 } catch (RuntimeException | OutOfMemoryError e) { 310 Log.w(TAG, "Unable to load default wallpaper!", e); 311 bitmap = null; 312 } 313 } 314 315 if (bitmap == null) { 316 Log.w(TAG, "Could not load bitmap"); 317 } else if (bitmap.isRecycled()) { 318 Log.e(TAG, "Attempt to load a recycled bitmap"); 319 } else if (mBitmap == bitmap) { 320 Log.e(TAG, "Loaded a bitmap that was already loaded"); 321 } else { 322 // at this point, loading is done correctly. 323 loadSuccess = true; 324 // recycle the previously loaded bitmap 325 if (mBitmap != null) { 326 mBitmap.recycle(); 327 } 328 mBitmap = bitmap; 329 mWideColorGamut = mWallpaperManager.wallpaperSupportsWcg( 330 WallpaperManager.FLAG_SYSTEM); 331 332 // +2 usages for the color extraction and the delayed unload. 333 mBitmapUsages += 2; 334 recomputeColorExtractorMiniBitmap(); 335 drawFrameInternal(); 336 337 /* 338 * after loading, the bitmap will be unloaded after all these conditions: 339 * - the frame is redrawn 340 * - the mini bitmap from color extractor is recomputed 341 * - the DELAY_UNLOAD_BITMAP has passed 342 */ 343 mLongExecutor.executeDelayed( 344 this::unloadBitmapIfNotUsedSynchronized, DELAY_UNLOAD_BITMAP); 345 } 346 // even if the bitmap cannot be loaded, call reportEngineShown 347 if (!loadSuccess) reportEngineShown(false); 348 Trace.endSection(); 349 } 350 onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors)351 private void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { 352 try { 353 notifyLocalColorsChanged(regions, colors); 354 } catch (RuntimeException e) { 355 Log.e(TAG, e.getMessage(), e); 356 } 357 } 358 359 @VisibleForTesting recomputeColorExtractorMiniBitmap()360 void recomputeColorExtractorMiniBitmap() { 361 mWallpaperLocalColorExtractor.onBitmapChanged(mBitmap); 362 } 363 364 @VisibleForTesting onMiniBitmapUpdated()365 void onMiniBitmapUpdated() { 366 unloadBitmapIfNotUsed(); 367 } 368 369 @Override supportsLocalColorExtraction()370 public boolean supportsLocalColorExtraction() { 371 return true; 372 } 373 374 @Override addLocalColorsAreas(@onNull List<RectF> regions)375 public void addLocalColorsAreas(@NonNull List<RectF> regions) { 376 // this call will activate the offset notifications 377 // if no colors were being processed before 378 mWallpaperLocalColorExtractor.addLocalColorsAreas(regions); 379 } 380 381 @Override removeLocalColorsAreas(@onNull List<RectF> regions)382 public void removeLocalColorsAreas(@NonNull List<RectF> regions) { 383 // this call will deactivate the offset notifications 384 // if we are no longer processing colors 385 mWallpaperLocalColorExtractor.removeLocalColorAreas(regions); 386 } 387 388 @Override onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset)389 public void onOffsetsChanged(float xOffset, float yOffset, 390 float xOffsetStep, float yOffsetStep, 391 int xPixelOffset, int yPixelOffset) { 392 final int pages; 393 if (xOffsetStep > 0 && xOffsetStep <= 1) { 394 pages = Math.round(1 / xOffsetStep) + 1; 395 } else { 396 pages = 1; 397 } 398 if (pages != mPages || !mPagesComputed) { 399 mPages = pages; 400 mPagesComputed = true; 401 mWallpaperLocalColorExtractor.onPageChanged(mPages); 402 } 403 } 404 405 @Override onDisplayAdded(int displayId)406 public void onDisplayAdded(int displayId) { 407 408 } 409 410 @Override onDisplayRemoved(int displayId)411 public void onDisplayRemoved(int displayId) { 412 413 } 414 415 @Override onDisplayChanged(int displayId)416 public void onDisplayChanged(int displayId) { 417 // changes the display in the color extractor 418 // the new display dimensions will be used in the next color computation 419 if (displayId == getDisplayContext().getDisplayId()) { 420 getDisplaySizeAndUpdateColorExtractor(); 421 } 422 } 423 getDisplaySizeAndUpdateColorExtractor()424 private void getDisplaySizeAndUpdateColorExtractor() { 425 Rect window = getDisplayContext() 426 .getSystemService(WindowManager.class) 427 .getCurrentWindowMetrics() 428 .getBounds(); 429 mWallpaperLocalColorExtractor.setDisplayDimensions(window.width(), window.height()); 430 } 431 432 @Override dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args)433 protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) { 434 super.dump(prefix, fd, out, args); 435 out.print(prefix); out.print("Engine="); out.println(this); 436 out.print(prefix); out.print("valid surface="); 437 out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null 438 ? getSurfaceHolder().getSurface().isValid() 439 : "null"); 440 441 out.print(prefix); out.print("surface frame="); 442 out.println(getSurfaceHolder() != null ? getSurfaceHolder().getSurfaceFrame() : "null"); 443 444 out.print(prefix); out.print("bitmap="); 445 out.println(mBitmap == null ? "null" 446 : mBitmap.isRecycled() ? "recycled" 447 : mBitmap.getWidth() + "x" + mBitmap.getHeight()); 448 449 mWallpaperLocalColorExtractor.dump(prefix, fd, out, args); 450 } 451 } 452 } 453