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