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;
18 
19 import android.app.WallpaperColors;
20 import android.graphics.Bitmap;
21 import android.graphics.Rect;
22 import android.graphics.RectF;
23 import android.hardware.display.DisplayManager;
24 import android.hardware.display.DisplayManager.DisplayListener;
25 import android.os.Handler;
26 import android.os.HandlerThread;
27 import android.os.SystemClock;
28 import android.os.Trace;
29 import android.service.wallpaper.WallpaperService;
30 import android.util.ArraySet;
31 import android.util.Log;
32 import android.util.MathUtils;
33 import android.util.Size;
34 import android.view.SurfaceHolder;
35 import android.view.WindowManager;
36 
37 import androidx.annotation.NonNull;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.systemui.glwallpaper.EglHelper;
41 import com.android.systemui.glwallpaper.ImageWallpaperRenderer;
42 
43 import java.io.FileDescriptor;
44 import java.io.PrintWriter;
45 import java.util.ArrayList;
46 import java.util.List;
47 
48 import javax.inject.Inject;
49 
50 /**
51  * Default built-in wallpaper that simply shows a static image.
52  */
53 @SuppressWarnings({"UnusedDeclaration"})
54 public class ImageWallpaper extends WallpaperService {
55     private static final String TAG = ImageWallpaper.class.getSimpleName();
56     // We delayed destroy render context that subsequent render requests have chance to cancel it.
57     // This is to avoid destroying then recreating render context in a very short time.
58     private static final int DELAY_FINISH_RENDERING = 1000;
59     private static final @android.annotation.NonNull RectF LOCAL_COLOR_BOUNDS =
60             new RectF(0, 0, 1, 1);
61     private static final boolean DEBUG = false;
62     private final ArrayList<RectF> mLocalColorsToAdd = new ArrayList<>();
63     private final ArraySet<RectF> mColorAreas = new ArraySet<>();
64     private volatile int mPages = 1;
65     private HandlerThread mWorker;
66     // scaled down version
67     private Bitmap mMiniBitmap;
68 
69     @Inject
ImageWallpaper()70     public ImageWallpaper() {
71         super();
72     }
73 
74     @Override
onCreate()75     public void onCreate() {
76         super.onCreate();
77         mWorker = new HandlerThread(TAG);
78         mWorker.start();
79     }
80 
81     @Override
onCreateEngine()82     public Engine onCreateEngine() {
83         return new GLEngine();
84     }
85 
86     @Override
onDestroy()87     public void onDestroy() {
88         super.onDestroy();
89         mWorker.quitSafely();
90         mWorker = null;
91         mMiniBitmap = null;
92     }
93 
94     class GLEngine extends Engine implements DisplayListener {
95         // Surface is rejected if size below a threshold on some devices (ie. 8px on elfin)
96         // set min to 64 px (CTS covers this), please refer to ag/4867989 for detail.
97         @VisibleForTesting
98         static final int MIN_SURFACE_WIDTH = 128;
99         @VisibleForTesting
100         static final int MIN_SURFACE_HEIGHT = 128;
101 
102         private ImageWallpaperRenderer mRenderer;
103         private EglHelper mEglHelper;
104         private final Runnable mFinishRenderingTask = this::finishRendering;
105         private boolean mNeedRedraw;
106 
107         private boolean mDisplaySizeValid = false;
108         private int mDisplayWidth = 1;
109         private int mDisplayHeight = 1;
110 
111         private int mImgWidth = 1;
112         private int mImgHeight = 1;
113 
GLEngine()114         GLEngine() { }
115 
116         @VisibleForTesting
GLEngine(Handler handler)117         GLEngine(Handler handler) {
118             super(SystemClock::elapsedRealtime, handler);
119         }
120 
121         @Override
onCreate(SurfaceHolder surfaceHolder)122         public void onCreate(SurfaceHolder surfaceHolder) {
123             Trace.beginSection("ImageWallpaper.Engine#onCreate");
124             mEglHelper = getEglHelperInstance();
125             // Deferred init renderer because we need to get wallpaper by display context.
126             mRenderer = getRendererInstance();
127             setFixedSizeAllowed(true);
128             updateSurfaceSize();
129 
130             mRenderer.setOnBitmapChanged(b -> {
131                 mLocalColorsToAdd.addAll(mColorAreas);
132                 if (mLocalColorsToAdd.size() > 0) {
133                     updateMiniBitmapAndNotify(b);
134                 }
135             });
136             getDisplayContext().getSystemService(DisplayManager.class)
137                     .registerDisplayListener(this, mWorker.getThreadHandler());
138             Trace.endSection();
139         }
140 
141         @Override
onDisplayAdded(int displayId)142         public void onDisplayAdded(int displayId) { }
143 
144         @Override
onDisplayRemoved(int displayId)145         public void onDisplayRemoved(int displayId) { }
146 
147         @Override
onDisplayChanged(int displayId)148         public void onDisplayChanged(int displayId) {
149             if (displayId == getDisplayContext().getDisplayId()) {
150                 mDisplaySizeValid = false;
151             }
152         }
153 
getEglHelperInstance()154         EglHelper getEglHelperInstance() {
155             return new EglHelper();
156         }
157 
getRendererInstance()158         ImageWallpaperRenderer getRendererInstance() {
159             return new ImageWallpaperRenderer(getDisplayContext());
160         }
161 
162         @Override
onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset)163         public void onOffsetsChanged(float xOffset, float yOffset,
164                 float xOffsetStep, float yOffsetStep,
165                 int xPixelOffset, int yPixelOffset) {
166             final int pages;
167             if (xOffsetStep > 0 && xOffsetStep <= 1) {
168                 pages = (int) Math.round(1 / xOffsetStep) + 1;
169             } else {
170                 pages = 1;
171             }
172             if (pages == mPages) return;
173             mPages = pages;
174             if (mMiniBitmap == null || mMiniBitmap.isRecycled()) return;
175             mWorker.getThreadHandler().post(() ->
176                     computeAndNotifyLocalColors(new ArrayList<>(mColorAreas), mMiniBitmap));
177         }
178 
updateMiniBitmapAndNotify(Bitmap b)179         private void updateMiniBitmapAndNotify(Bitmap b) {
180             if (b == null) return;
181             int size = Math.min(b.getWidth(), b.getHeight());
182             float scale = 1.0f;
183             if (size > MIN_SURFACE_WIDTH) {
184                 scale = (float) MIN_SURFACE_WIDTH / (float) size;
185             }
186             mImgHeight = b.getHeight();
187             mImgWidth = b.getWidth();
188             mMiniBitmap = Bitmap.createScaledBitmap(b,  (int) Math.max(scale * b.getWidth(), 1),
189                     (int) Math.max(scale * b.getHeight(), 1), false);
190             computeAndNotifyLocalColors(mLocalColorsToAdd, mMiniBitmap);
191             mLocalColorsToAdd.clear();
192         }
193 
updateSurfaceSize()194         private void updateSurfaceSize() {
195             Trace.beginSection("ImageWallpaper#updateSurfaceSize");
196             SurfaceHolder holder = getSurfaceHolder();
197             Size frameSize = mRenderer.reportSurfaceSize();
198             int width = Math.max(MIN_SURFACE_WIDTH, frameSize.getWidth());
199             int height = Math.max(MIN_SURFACE_HEIGHT, frameSize.getHeight());
200             holder.setFixedSize(width, height);
201             Trace.endSection();
202         }
203 
204         @Override
shouldZoomOutWallpaper()205         public boolean shouldZoomOutWallpaper() {
206             return true;
207         }
208 
209         @Override
shouldWaitForEngineShown()210         public boolean shouldWaitForEngineShown() {
211             return true;
212         }
213 
214         @Override
onDestroy()215         public void onDestroy() {
216             getDisplayContext().getSystemService(DisplayManager.class)
217                     .unregisterDisplayListener(this);
218             mMiniBitmap = null;
219             mWorker.getThreadHandler().post(() -> {
220                 mRenderer.finish();
221                 mRenderer = null;
222                 mEglHelper.finish();
223                 mEglHelper = null;
224             });
225         }
226 
227         @Override
supportsLocalColorExtraction()228         public boolean supportsLocalColorExtraction() {
229             return true;
230         }
231 
232         @Override
addLocalColorsAreas(@onNull List<RectF> regions)233         public void addLocalColorsAreas(@NonNull List<RectF> regions) {
234             mWorker.getThreadHandler().post(() -> {
235                 if (mColorAreas.size() + mLocalColorsToAdd.size() == 0) {
236                     setOffsetNotificationsEnabled(true);
237                 }
238                 Bitmap bitmap = mMiniBitmap;
239                 if (bitmap == null) {
240                     mLocalColorsToAdd.addAll(regions);
241                     if (mRenderer != null) mRenderer.use(this::updateMiniBitmapAndNotify);
242                 } else {
243                     computeAndNotifyLocalColors(regions, bitmap);
244                 }
245             });
246         }
247 
computeAndNotifyLocalColors(@onNull List<RectF> regions, Bitmap b)248         private void computeAndNotifyLocalColors(@NonNull List<RectF> regions, Bitmap b) {
249             List<WallpaperColors> colors = getLocalWallpaperColors(regions, b);
250             mColorAreas.addAll(regions);
251             try {
252                 notifyLocalColorsChanged(regions, colors);
253             } catch (RuntimeException e) {
254                 Log.e(TAG, e.getMessage(), e);
255             }
256         }
257 
258         @Override
removeLocalColorsAreas(@onNull List<RectF> regions)259         public void removeLocalColorsAreas(@NonNull List<RectF> regions) {
260             mWorker.getThreadHandler().post(() -> {
261                 mColorAreas.removeAll(regions);
262                 mLocalColorsToAdd.removeAll(regions);
263                 if (mColorAreas.size() + mLocalColorsToAdd.size() == 0) {
264                     setOffsetNotificationsEnabled(false);
265                 }
266             });
267         }
268 
269         /**
270          * Transform the logical coordinates into wallpaper coordinates.
271          *
272          * Logical coordinates are organised such that the various pages are non-overlapping. So,
273          * if there are n pages, the first page will have its X coordinate on the range [0-1/n].
274          *
275          * The real pages are overlapping. If the Wallpaper are a width Ww and the screen a width
276          * Ws, the relative width of a page Wr is Ws/Ww. This does not change if the number of
277          * pages increase.
278          * If there are n pages, the page k starts at the offset k * (1 - Wr) / (n - 1), as the
279          * last page is at position (1-Wr) and the others are regularly spread on the range [0-
280          * (1-Wr)].
281          */
pageToImgRect(RectF area)282         private RectF pageToImgRect(RectF area) {
283             if (!mDisplaySizeValid) {
284                 Rect window = getDisplayContext()
285                         .getSystemService(WindowManager.class)
286                         .getCurrentWindowMetrics()
287                         .getBounds();
288                 mDisplayWidth = window.width();
289                 mDisplayHeight = window.height();
290                 mDisplaySizeValid = true;
291             }
292 
293             // Width of a page for the caller of this API.
294             float virtualPageWidth = 1f / (float) mPages;
295             float leftPosOnPage = (area.left % virtualPageWidth) / virtualPageWidth;
296             float rightPosOnPage = (area.right % virtualPageWidth) / virtualPageWidth;
297             int currentPage = (int) Math.floor(area.centerX() / virtualPageWidth);
298 
299             RectF imgArea = new RectF();
300 
301             if (mImgWidth == 0 || mImgHeight == 0 || mDisplayWidth <= 0 || mDisplayHeight <= 0) {
302                 return imgArea;
303             }
304 
305             imgArea.bottom = area.bottom;
306             imgArea.top = area.top;
307 
308             float imageScale = Math.min(((float) mImgHeight) / mDisplayHeight, 1);
309             float mappedScreenWidth = mDisplayWidth * imageScale;
310             float pageWidth = Math.min(1.0f,
311                     mImgWidth > 0 ? mappedScreenWidth / (float) mImgWidth : 1.f);
312             float pageOffset = (1 - pageWidth) / (float) (mPages - 1);
313 
314             imgArea.left = MathUtils.constrain(
315                     leftPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
316             imgArea.right = MathUtils.constrain(
317                     rightPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
318             if (imgArea.left > imgArea.right) {
319                 // take full page
320                 imgArea.left = 0;
321                 imgArea.right = 1;
322             }
323 
324             return imgArea;
325         }
326 
getLocalWallpaperColors(@onNull List<RectF> areas, Bitmap b)327         private List<WallpaperColors> getLocalWallpaperColors(@NonNull List<RectF> areas,
328                 Bitmap b) {
329             List<WallpaperColors> colors = new ArrayList<>(areas.size());
330             for (int i = 0; i < areas.size(); i++) {
331                 RectF area = pageToImgRect(areas.get(i));
332                 if (area == null || !LOCAL_COLOR_BOUNDS.contains(area)) {
333                     colors.add(null);
334                     continue;
335                 }
336                 Rect subImage = new Rect(
337                         (int) Math.floor(area.left * b.getWidth()),
338                         (int) Math.floor(area.top * b.getHeight()),
339                         (int) Math.ceil(area.right * b.getWidth()),
340                         (int) Math.ceil(area.bottom * b.getHeight()));
341                 if (subImage.isEmpty()) {
342                     // Do not notify client. treat it as too small to sample
343                     colors.add(null);
344                     continue;
345                 }
346                 Bitmap colorImg = Bitmap.createBitmap(b,
347                         subImage.left, subImage.top, subImage.width(), subImage.height());
348                 WallpaperColors color = WallpaperColors.fromBitmap(colorImg);
349                 colors.add(color);
350             }
351             return colors;
352         }
353 
354         @Override
onSurfaceCreated(SurfaceHolder holder)355         public void onSurfaceCreated(SurfaceHolder holder) {
356             if (mWorker == null) return;
357             mWorker.getThreadHandler().post(() -> {
358                 Trace.beginSection("ImageWallpaper#onSurfaceCreated");
359                 mEglHelper.init(holder, needSupportWideColorGamut());
360                 mRenderer.onSurfaceCreated();
361                 Trace.endSection();
362             });
363         }
364 
365         @Override
onSurfaceChanged(SurfaceHolder holder, int format, int width, int height)366         public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
367             if (mWorker == null) return;
368             mWorker.getThreadHandler().post(() -> mRenderer.onSurfaceChanged(width, height));
369         }
370 
371         @Override
onSurfaceRedrawNeeded(SurfaceHolder holder)372         public void onSurfaceRedrawNeeded(SurfaceHolder holder) {
373             if (mWorker == null) return;
374             mWorker.getThreadHandler().post(this::drawFrame);
375         }
376 
drawFrame()377         private void drawFrame() {
378             Trace.beginSection("ImageWallpaper#drawFrame");
379             preRender();
380             requestRender();
381             postRender();
382             Trace.endSection();
383         }
384 
preRender()385         public void preRender() {
386             // This method should only be invoked from worker thread.
387             Trace.beginSection("ImageWallpaper#preRender");
388             preRenderInternal();
389             Trace.endSection();
390         }
391 
preRenderInternal()392         private void preRenderInternal() {
393             boolean contextRecreated = false;
394             Rect frame = getSurfaceHolder().getSurfaceFrame();
395             cancelFinishRenderingTask();
396 
397             // Check if we need to recreate egl context.
398             if (!mEglHelper.hasEglContext()) {
399                 mEglHelper.destroyEglSurface();
400                 if (!mEglHelper.createEglContext()) {
401                     Log.w(TAG, "recreate egl context failed!");
402                 } else {
403                     contextRecreated = true;
404                 }
405             }
406 
407             // Check if we need to recreate egl surface.
408             if (mEglHelper.hasEglContext() && !mEglHelper.hasEglSurface()) {
409                 if (!mEglHelper.createEglSurface(getSurfaceHolder(), needSupportWideColorGamut())) {
410                     Log.w(TAG, "recreate egl surface failed!");
411                 }
412             }
413 
414             // If we recreate egl context, notify renderer to setup again.
415             if (mEglHelper.hasEglContext() && mEglHelper.hasEglSurface() && contextRecreated) {
416                 mRenderer.onSurfaceCreated();
417                 mRenderer.onSurfaceChanged(frame.width(), frame.height());
418             }
419         }
420 
requestRender()421         public void requestRender() {
422             // This method should only be invoked from worker thread.
423             Trace.beginSection("ImageWallpaper#requestRender");
424             requestRenderInternal();
425             Trace.endSection();
426         }
427 
requestRenderInternal()428         private void requestRenderInternal() {
429             Rect frame = getSurfaceHolder().getSurfaceFrame();
430             boolean readyToRender = mEglHelper.hasEglContext() && mEglHelper.hasEglSurface()
431                     && frame.width() > 0 && frame.height() > 0;
432 
433             if (readyToRender) {
434                 mRenderer.onDrawFrame();
435                 if (!mEglHelper.swapBuffer()) {
436                     Log.e(TAG, "drawFrame failed!");
437                 }
438             } else {
439                 Log.e(TAG, "requestRender: not ready, has context=" + mEglHelper.hasEglContext()
440                         + ", has surface=" + mEglHelper.hasEglSurface()
441                         + ", frame=" + frame);
442             }
443         }
444 
postRender()445         public void postRender() {
446             // This method should only be invoked from worker thread.
447             scheduleFinishRendering();
448             reportEngineShown(false /* waitForEngineShown */);
449         }
450 
cancelFinishRenderingTask()451         private void cancelFinishRenderingTask() {
452             if (mWorker == null) return;
453             mWorker.getThreadHandler().removeCallbacks(mFinishRenderingTask);
454         }
455 
scheduleFinishRendering()456         private void scheduleFinishRendering() {
457             if (mWorker == null) return;
458             cancelFinishRenderingTask();
459             mWorker.getThreadHandler().postDelayed(mFinishRenderingTask, DELAY_FINISH_RENDERING);
460         }
461 
finishRendering()462         private void finishRendering() {
463             Trace.beginSection("ImageWallpaper#finishRendering");
464             if (mEglHelper != null) {
465                 mEglHelper.destroyEglSurface();
466                 mEglHelper.destroyEglContext();
467             }
468             Trace.endSection();
469         }
470 
needSupportWideColorGamut()471         private boolean needSupportWideColorGamut() {
472             return mRenderer.isWcgContent();
473         }
474 
475         @Override
dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args)476         protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
477             super.dump(prefix, fd, out, args);
478             out.print(prefix); out.print("Engine="); out.println(this);
479             out.print(prefix); out.print("valid surface=");
480             out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null
481                     ? getSurfaceHolder().getSurface().isValid()
482                     : "null");
483 
484             out.print(prefix); out.print("surface frame=");
485             out.println(getSurfaceHolder() != null ? getSurfaceHolder().getSurfaceFrame() : "null");
486 
487             mEglHelper.dump(prefix, fd, out, args);
488             mRenderer.dump(prefix, fd, out, args);
489         }
490     }
491 }
492