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