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