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