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