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
17package com.android.systemui;
18
19import android.app.WallpaperManager;
20import android.content.ComponentCallbacks2;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Rect;
24import android.graphics.RectF;
25import android.graphics.Region.Op;
26import android.os.AsyncTask;
27import android.os.Handler;
28import android.os.Trace;
29import android.service.wallpaper.WallpaperService;
30import android.util.Log;
31import android.view.Display;
32import android.view.DisplayInfo;
33import android.view.Surface;
34import android.view.SurfaceHolder;
35import android.view.WindowManager;
36
37import java.io.FileDescriptor;
38import java.io.IOException;
39import java.io.PrintWriter;
40
41/**
42 * Default built-in wallpaper that simply shows a static image.
43 */
44@SuppressWarnings({"UnusedDeclaration"})
45public class ImageWallpaper extends WallpaperService {
46    private static final String TAG = "ImageWallpaper";
47    private static final String GL_LOG_TAG = "ImageWallpaperGL";
48    private static final boolean DEBUG = false;
49    private static final String PROPERTY_KERNEL_QEMU = "ro.kernel.qemu";
50    private static final long DELAY_FORGET_WALLPAPER = 5000;
51
52    private WallpaperManager mWallpaperManager;
53    private DrawableEngine mEngine;
54
55    @Override
56    public void onCreate() {
57        super.onCreate();
58        mWallpaperManager = getSystemService(WallpaperManager.class);
59    }
60
61    @Override
62    public void onTrimMemory(int level) {
63        if (mEngine != null) {
64            mEngine.trimMemory(level);
65        }
66    }
67
68    @Override
69    public Engine onCreateEngine() {
70        mEngine = new DrawableEngine();
71        return mEngine;
72    }
73
74    class DrawableEngine extends Engine {
75        private final Runnable mUnloadWallpaperCallback = () -> {
76            unloadWallpaper(false /* forgetSize */);
77        };
78
79        Bitmap mBackground;
80        int mBackgroundWidth = -1, mBackgroundHeight = -1;
81        int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1;
82        int mLastRotation = -1;
83        float mXOffset = 0f;
84        float mYOffset = 0f;
85        float mScale = 1f;
86
87        private Display mDefaultDisplay;
88        private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
89
90        boolean mVisible = true;
91        boolean mOffsetsChanged;
92        int mLastXTranslation;
93        int mLastYTranslation;
94
95        private int mRotationAtLastSurfaceSizeUpdate = -1;
96        private int mDisplayWidthAtLastSurfaceSizeUpdate = -1;
97        private int mDisplayHeightAtLastSurfaceSizeUpdate = -1;
98
99        private int mLastRequestedWidth = -1;
100        private int mLastRequestedHeight = -1;
101        private AsyncTask<Void, Void, Bitmap> mLoader;
102        private boolean mNeedsDrawAfterLoadingWallpaper;
103        private boolean mSurfaceValid;
104        private boolean mSurfaceRedrawNeeded;
105
106        DrawableEngine() {
107            super();
108            setFixedSizeAllowed(true);
109        }
110
111        void trimMemory(int level) {
112            if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW
113                    && level <= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL
114                    && mBackground != null) {
115                if (DEBUG) {
116                    Log.d(TAG, "trimMemory");
117                }
118                unloadWallpaper(true /* forgetSize */);
119            }
120        }
121
122        @Override
123        public void onCreate(SurfaceHolder surfaceHolder) {
124            if (DEBUG) {
125                Log.d(TAG, "onCreate");
126            }
127
128            super.onCreate(surfaceHolder);
129
130            //noinspection ConstantConditions
131            mDefaultDisplay = getSystemService(WindowManager.class).getDefaultDisplay();
132            setOffsetNotificationsEnabled(false);
133
134            updateSurfaceSize(surfaceHolder, getDefaultDisplayInfo(), false /* forDraw */);
135        }
136
137        @Override
138        public void onDestroy() {
139            super.onDestroy();
140            mBackground = null;
141            unloadWallpaper(true /* forgetSize */);
142        }
143
144        boolean updateSurfaceSize(SurfaceHolder surfaceHolder, DisplayInfo displayInfo,
145                boolean forDraw) {
146            boolean hasWallpaper = true;
147
148            // Load background image dimensions, if we haven't saved them yet
149            if (mBackgroundWidth <= 0 || mBackgroundHeight <= 0) {
150                // Need to load the image to get dimensions
151                loadWallpaper(forDraw);
152                if (DEBUG) {
153                    Log.d(TAG, "Reloading, redoing updateSurfaceSize later.");
154                }
155                hasWallpaper = false;
156            }
157
158            // Force the wallpaper to cover the screen in both dimensions
159            int surfaceWidth = Math.max(displayInfo.logicalWidth, mBackgroundWidth);
160            int surfaceHeight = Math.max(displayInfo.logicalHeight, mBackgroundHeight);
161
162            // Used a fixed size surface, because we are special.  We can do
163            // this because we know the current design of window animations doesn't
164            // cause this to break.
165            surfaceHolder.setFixedSize(surfaceWidth, surfaceHeight);
166            mLastRequestedWidth = surfaceWidth;
167            mLastRequestedHeight = surfaceHeight;
168
169            return hasWallpaper;
170        }
171
172        @Override
173        public void onVisibilityChanged(boolean visible) {
174            if (DEBUG) {
175                Log.d(TAG, "onVisibilityChanged: mVisible, visible=" + mVisible + ", " + visible);
176            }
177
178            if (mVisible != visible) {
179                if (DEBUG) {
180                    Log.d(TAG, "Visibility changed to visible=" + visible);
181                }
182                mVisible = visible;
183                if (visible) {
184                    drawFrame();
185                }
186            }
187        }
188
189        @Override
190        public void onOffsetsChanged(float xOffset, float yOffset,
191                float xOffsetStep, float yOffsetStep,
192                int xPixels, int yPixels) {
193            if (DEBUG) {
194                Log.d(TAG, "onOffsetsChanged: xOffset=" + xOffset + ", yOffset=" + yOffset
195                        + ", xOffsetStep=" + xOffsetStep + ", yOffsetStep=" + yOffsetStep
196                        + ", xPixels=" + xPixels + ", yPixels=" + yPixels);
197            }
198
199            if (mXOffset != xOffset || mYOffset != yOffset) {
200                if (DEBUG) {
201                    Log.d(TAG, "Offsets changed to (" + xOffset + "," + yOffset + ").");
202                }
203                mXOffset = xOffset;
204                mYOffset = yOffset;
205                mOffsetsChanged = true;
206            }
207            drawFrame();
208        }
209
210        @Override
211        public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
212            if (DEBUG) {
213                Log.d(TAG, "onSurfaceChanged: width=" + width + ", height=" + height);
214            }
215
216            super.onSurfaceChanged(holder, format, width, height);
217
218            drawFrame();
219        }
220
221        @Override
222        public void onSurfaceDestroyed(SurfaceHolder holder) {
223            super.onSurfaceDestroyed(holder);
224            if (DEBUG) {
225                Log.i(TAG, "onSurfaceDestroyed");
226            }
227
228            mLastSurfaceWidth = mLastSurfaceHeight = -1;
229            mSurfaceValid = false;
230        }
231
232        @Override
233        public void onSurfaceCreated(SurfaceHolder holder) {
234            super.onSurfaceCreated(holder);
235            if (DEBUG) {
236                Log.i(TAG, "onSurfaceCreated");
237            }
238
239            mLastSurfaceWidth = mLastSurfaceHeight = -1;
240            mSurfaceValid = true;
241        }
242
243        @Override
244        public void onSurfaceRedrawNeeded(SurfaceHolder holder) {
245            if (DEBUG) {
246                Log.d(TAG, "onSurfaceRedrawNeeded");
247            }
248            super.onSurfaceRedrawNeeded(holder);
249            // At the end of this method we should have drawn into the surface.
250            // This means that the bitmap should be loaded synchronously if
251            // it was already unloaded.
252            if (mBackground == null) {
253                updateBitmap(mWallpaperManager.getBitmap(true /* hardware */));
254            }
255            mSurfaceRedrawNeeded = true;
256            drawFrame();
257        }
258
259        private DisplayInfo getDefaultDisplayInfo() {
260            mDefaultDisplay.getDisplayInfo(mTmpDisplayInfo);
261            return mTmpDisplayInfo;
262        }
263
264        void drawFrame() {
265            if (!mSurfaceValid) {
266                return;
267            }
268            try {
269                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "drawWallpaper");
270                DisplayInfo displayInfo = getDefaultDisplayInfo();
271                int newRotation = displayInfo.rotation;
272
273                // Sometimes a wallpaper is not large enough to cover the screen in one dimension.
274                // Call updateSurfaceSize -- it will only actually do the update if the dimensions
275                // should change
276                if (newRotation != mLastRotation) {
277                    // Update surface size (if necessary)
278                    if (!updateSurfaceSize(getSurfaceHolder(), displayInfo, true /* forDraw */)) {
279                        return; // had to reload wallpaper, will retry later
280                    }
281                    mRotationAtLastSurfaceSizeUpdate = newRotation;
282                    mDisplayWidthAtLastSurfaceSizeUpdate = displayInfo.logicalWidth;
283                    mDisplayHeightAtLastSurfaceSizeUpdate = displayInfo.logicalHeight;
284                }
285                SurfaceHolder sh = getSurfaceHolder();
286                final Rect frame = sh.getSurfaceFrame();
287                final int dw = frame.width();
288                final int dh = frame.height();
289                boolean surfaceDimensionsChanged = dw != mLastSurfaceWidth
290                        || dh != mLastSurfaceHeight;
291
292                boolean redrawNeeded = surfaceDimensionsChanged || newRotation != mLastRotation
293                        || mSurfaceRedrawNeeded;
294                if (!redrawNeeded && !mOffsetsChanged) {
295                    if (DEBUG) {
296                        Log.d(TAG, "Suppressed drawFrame since redraw is not needed "
297                                + "and offsets have not changed.");
298                    }
299                    return;
300                }
301                mLastRotation = newRotation;
302                mSurfaceRedrawNeeded = false;
303
304                // Load bitmap if it is not yet loaded
305                if (mBackground == null) {
306                    loadWallpaper(true);
307                    if (DEBUG) {
308                        Log.d(TAG, "Reloading, resuming draw later");
309                    }
310                    return;
311                }
312
313                // Left align the scaled image
314                mScale = Math.max(1f, Math.max(dw / (float) mBackground.getWidth(),
315                        dh / (float) mBackground.getHeight()));
316                final int availw = (int) (mBackground.getWidth() * mScale) - dw;
317                final int availh = (int) (mBackground.getHeight() * mScale) - dh;
318                int xPixels = (int) (availw * mXOffset);
319                int yPixels = (int) (availh * mYOffset);
320
321                mOffsetsChanged = false;
322                if (surfaceDimensionsChanged) {
323                    mLastSurfaceWidth = dw;
324                    mLastSurfaceHeight = dh;
325                }
326                if (!redrawNeeded && xPixels == mLastXTranslation && yPixels == mLastYTranslation) {
327                    if (DEBUG) {
328                        Log.d(TAG, "Suppressed drawFrame since the image has not "
329                                + "actually moved an integral number of pixels.");
330                    }
331                    return;
332                }
333                mLastXTranslation = xPixels;
334                mLastYTranslation = yPixels;
335
336                if (DEBUG) {
337                    Log.d(TAG, "Redrawing wallpaper");
338                }
339
340                drawWallpaperWithCanvas(sh, availw, availh, xPixels, yPixels);
341                scheduleUnloadWallpaper();
342            } finally {
343                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
344            }
345        }
346
347        /**
348         * Loads the wallpaper on background thread and schedules updating the surface frame,
349         * and if {@param needsDraw} is set also draws a frame.
350         *
351         * If loading is already in-flight, subsequent loads are ignored (but needDraw is or-ed to
352         * the active request).
353         *
354         * If {@param needsReset} is set also clears the cache in WallpaperManager first.
355         */
356        private void loadWallpaper(boolean needsDraw) {
357            mNeedsDrawAfterLoadingWallpaper |= needsDraw;
358            if (mLoader != null) {
359                if (DEBUG) {
360                    Log.d(TAG, "Skipping loadWallpaper, already in flight ");
361                }
362                return;
363            }
364            mLoader = new AsyncTask<Void, Void, Bitmap>() {
365                @Override
366                protected Bitmap doInBackground(Void... params) {
367                    Throwable exception;
368                    try {
369                        return mWallpaperManager.getBitmap(true /* hardware */);
370                    } catch (RuntimeException | OutOfMemoryError e) {
371                        exception = e;
372                    }
373
374                    if (isCancelled()) {
375                        return null;
376                    }
377
378                    // Note that if we do fail at this, and the default wallpaper can't
379                    // be loaded, we will go into a cycle.  Don't do a build where the
380                    // default wallpaper can't be loaded.
381                    Log.w(TAG, "Unable to load wallpaper!", exception);
382                    try {
383                        mWallpaperManager.clear();
384                    } catch (IOException ex) {
385                        // now we're really screwed.
386                        Log.w(TAG, "Unable reset to default wallpaper!", ex);
387                    }
388
389                    if (isCancelled()) {
390                        return null;
391                    }
392
393                    try {
394                        return mWallpaperManager.getBitmap(true /* hardware */);
395                    } catch (RuntimeException | OutOfMemoryError e) {
396                        Log.w(TAG, "Unable to load default wallpaper!", e);
397                    }
398                    return null;
399                }
400
401                @Override
402                protected void onPostExecute(Bitmap b) {
403                    updateBitmap(b);
404
405                    if (mNeedsDrawAfterLoadingWallpaper) {
406                        drawFrame();
407                    }
408
409                    mLoader = null;
410                    mNeedsDrawAfterLoadingWallpaper = false;
411                }
412            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
413        }
414
415        private void updateBitmap(Bitmap bitmap) {
416            mBackground = null;
417            mBackgroundWidth = -1;
418            mBackgroundHeight = -1;
419
420            if (bitmap != null) {
421                mBackground = bitmap;
422                mBackgroundWidth = mBackground.getWidth();
423                mBackgroundHeight = mBackground.getHeight();
424            }
425
426            if (DEBUG) {
427                Log.d(TAG, "Wallpaper loaded: " + mBackground);
428            }
429            updateSurfaceSize(getSurfaceHolder(), getDefaultDisplayInfo(),
430                    false /* forDraw */);
431        }
432
433        private void unloadWallpaper(boolean forgetSize) {
434            if (mLoader != null) {
435                mLoader.cancel(false);
436                mLoader = null;
437            }
438            mBackground = null;
439            if (forgetSize) {
440                mBackgroundWidth = -1;
441                mBackgroundHeight = -1;
442            }
443
444            final Surface surface = getSurfaceHolder().getSurface();
445            surface.hwuiDestroy();
446
447            mWallpaperManager.forgetLoadedWallpaper();
448        }
449
450        private void scheduleUnloadWallpaper() {
451            Handler handler = getMainThreadHandler();
452            handler.removeCallbacks(mUnloadWallpaperCallback);
453            handler.postDelayed(mUnloadWallpaperCallback, DELAY_FORGET_WALLPAPER);
454        }
455
456        @Override
457        protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
458            super.dump(prefix, fd, out, args);
459
460            out.print(prefix); out.println("ImageWallpaper.DrawableEngine:");
461            out.print(prefix); out.print(" mBackground="); out.print(mBackground);
462            out.print(" mBackgroundWidth="); out.print(mBackgroundWidth);
463            out.print(" mBackgroundHeight="); out.println(mBackgroundHeight);
464
465            out.print(prefix); out.print(" mLastRotation="); out.print(mLastRotation);
466            out.print(" mLastSurfaceWidth="); out.print(mLastSurfaceWidth);
467            out.print(" mLastSurfaceHeight="); out.println(mLastSurfaceHeight);
468
469            out.print(prefix); out.print(" mXOffset="); out.print(mXOffset);
470            out.print(" mYOffset="); out.println(mYOffset);
471
472            out.print(prefix); out.print(" mVisible="); out.print(mVisible);
473            out.print(" mOffsetsChanged="); out.println(mOffsetsChanged);
474
475            out.print(prefix); out.print(" mLastXTranslation="); out.print(mLastXTranslation);
476            out.print(" mLastYTranslation="); out.print(mLastYTranslation);
477            out.print(" mScale="); out.println(mScale);
478
479            out.print(prefix); out.print(" mLastRequestedWidth="); out.print(mLastRequestedWidth);
480            out.print(" mLastRequestedHeight="); out.println(mLastRequestedHeight);
481
482            out.print(prefix); out.println(" DisplayInfo at last updateSurfaceSize:");
483            out.print(prefix);
484            out.print("  rotation="); out.print(mRotationAtLastSurfaceSizeUpdate);
485            out.print("  width="); out.print(mDisplayWidthAtLastSurfaceSizeUpdate);
486            out.print("  height="); out.println(mDisplayHeightAtLastSurfaceSizeUpdate);
487        }
488
489        private void drawWallpaperWithCanvas(SurfaceHolder sh, int w, int h, int left, int top) {
490            Canvas c = sh.lockHardwareCanvas();
491            if (c != null) {
492                try {
493                    if (DEBUG) {
494                        Log.d(TAG, "Redrawing: left=" + left + ", top=" + top);
495                    }
496
497                    final float right = left + mBackground.getWidth() * mScale;
498                    final float bottom = top + mBackground.getHeight() * mScale;
499                    if (w < 0 || h < 0) {
500                        c.save(Canvas.CLIP_SAVE_FLAG);
501                        c.clipRect(left, top, right, bottom,
502                                Op.DIFFERENCE);
503                        c.drawColor(0xff000000);
504                        c.restore();
505                    }
506                    if (mBackground != null) {
507                        RectF dest = new RectF(left, top, right, bottom);
508                        Log.i(TAG, "Redrawing in rect: " + dest + " with surface size: "
509                                + mLastRequestedWidth + "x" + mLastRequestedHeight);
510                        c.drawBitmap(mBackground, null, dest, null);
511                    }
512                } finally {
513                    sh.unlockCanvasAndPost(c);
514                }
515            }
516        }
517    }
518}
519