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