blob: fa3405fab8df4d50db901b324794ee1525970b19 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.HandlerThread;
import android.os.Trace;
import android.service.wallpaper.WallpaperService;
import android.util.Log;
import android.util.Size;
import android.view.DisplayInfo;
import android.view.SurfaceHolder;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.glwallpaper.EglHelper;
import com.android.systemui.glwallpaper.GLWallpaperRenderer;
import com.android.systemui.glwallpaper.ImageWallpaperRenderer;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.DozeParameters;
import java.io.FileDescriptor;
import java.io.PrintWriter;
/**
* Default built-in wallpaper that simply shows a static image.
*/
@SuppressWarnings({"UnusedDeclaration"})
public class ImageWallpaper extends WallpaperService {
private static final String TAG = ImageWallpaper.class.getSimpleName();
// We delayed destroy render context that subsequent render requests have chance to cancel it.
// This is to avoid destroying then recreating render context in a very short time.
private static final int DELAY_FINISH_RENDERING = 1000;
private static final int INTERVAL_WAIT_FOR_RENDERING = 100;
private static final int PATIENCE_WAIT_FOR_RENDERING = 10;
private static final boolean DEBUG = true;
private HandlerThread mWorker;
@Override
public void onCreate() {
super.onCreate();
mWorker = new HandlerThread(TAG);
mWorker.start();
}
@Override
public Engine onCreateEngine() {
return new GLEngine(this);
}
@Override
public void onDestroy() {
super.onDestroy();
mWorker.quitSafely();
mWorker = null;
}
class GLEngine extends Engine implements GLWallpaperRenderer.SurfaceProxy, StateListener {
// Surface is rejected if size below a threshold on some devices (ie. 8px on elfin)
// set min to 64 px (CTS covers this), please refer to ag/4867989 for detail.
@VisibleForTesting
static final int MIN_SURFACE_WIDTH = 64;
@VisibleForTesting
static final int MIN_SURFACE_HEIGHT = 64;
private GLWallpaperRenderer mRenderer;
private EglHelper mEglHelper;
private StatusBarStateController mController;
private final Runnable mFinishRenderingTask = this::finishRendering;
private final boolean mNeedTransition;
private boolean mShouldStopTransition;
private final boolean mIsHighEndGfx;
private final boolean mDisplayNeedsBlanking;
private final DisplayInfo mDisplayInfo = new DisplayInfo();
private final Object mMonitor = new Object();
private boolean mNeedRedraw;
// This variable can only be accessed in synchronized block.
private boolean mWaitingForRendering;
GLEngine(Context context) {
mIsHighEndGfx = ActivityManager.isHighEndGfx();
mDisplayNeedsBlanking = DozeParameters.getInstance(context).getDisplayNeedsBlanking();
mNeedTransition = mIsHighEndGfx && !mDisplayNeedsBlanking;
// We will preserve EGL context when we are in lock screen or aod
// to avoid janking in following transition, we need to release when back to home.
mController = Dependency.get(StatusBarStateController.class);
if (mController != null) {
mController.addCallback(this /* StateListener */);
}
}
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
mEglHelper = new EglHelper();
// Deferred init renderer because we need to get wallpaper by display context.
mRenderer = new ImageWallpaperRenderer(getDisplayContext(), this /* SurfaceProxy */);
getDisplayContext().getDisplay().getDisplayInfo(mDisplayInfo);
setFixedSizeAllowed(true);
setOffsetNotificationsEnabled(true);
updateSurfaceSize();
}
private void updateSurfaceSize() {
SurfaceHolder holder = getSurfaceHolder();
Size frameSize = mRenderer.reportSurfaceSize();
int width = Math.max(MIN_SURFACE_WIDTH, frameSize.getWidth());
int height = Math.max(MIN_SURFACE_HEIGHT, frameSize.getHeight());
holder.setFixedSize(width, height);
}
/**
* Check if necessary to stop transition with current wallpaper on this device. <br/>
* This should only be invoked after {@link #onSurfaceCreated(SurfaceHolder)}}
* is invoked since it needs display context and surface frame size.
*
* @return true if need to stop transition
*/
@VisibleForTesting
boolean checkIfShouldStopTransition() {
int orientation = getDisplayContext().getResources().getConfiguration().orientation;
boolean portrait = orientation == Configuration.ORIENTATION_PORTRAIT;
Rect frame = getSurfaceHolder().getSurfaceFrame();
int frameWidth = frame.width();
int frameHeight = frame.height();
int displayWidth = portrait ? mDisplayInfo.logicalWidth : mDisplayInfo.logicalHeight;
int displayHeight = portrait ? mDisplayInfo.logicalHeight : mDisplayInfo.logicalWidth;
return mNeedTransition
&& (frameWidth < displayWidth || frameHeight < displayHeight);
}
@Override
public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep,
float yOffsetStep, int xPixelOffset, int yPixelOffset) {
if (mWorker == null) return;
mWorker.getThreadHandler().post(() -> mRenderer.updateOffsets(xOffset, yOffset));
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) {
if (mWorker == null || !mNeedTransition) return;
final long duration = mShouldStopTransition ? 0 : animationDuration;
if (DEBUG) {
Log.d(TAG, "onAmbientModeChanged: inAmbient=" + inAmbientMode
+ ", duration=" + duration
+ ", mShouldStopTransition=" + mShouldStopTransition);
}
mWorker.getThreadHandler().post(
() -> mRenderer.updateAmbientMode(inAmbientMode, duration));
if (inAmbientMode && animationDuration == 0) {
// This means that we are transiting from home to aod, to avoid
// race condition between window visibility and transition,
// we don't return until the transition is finished. See b/136643341.
waitForBackgroundRendering();
}
}
private void waitForBackgroundRendering() {
synchronized (mMonitor) {
try {
mWaitingForRendering = true;
for (int patience = 1; mWaitingForRendering; patience++) {
mMonitor.wait(INTERVAL_WAIT_FOR_RENDERING);
mWaitingForRendering &= patience < PATIENCE_WAIT_FOR_RENDERING;
}
} catch (InterruptedException ex) {
} finally {
mWaitingForRendering = false;
}
}
}
@Override
public void onDestroy() {
if (mController != null) {
mController.removeCallback(this /* StateListener */);
}
mController = null;
mWorker.getThreadHandler().post(() -> {
mRenderer.finish();
mRenderer = null;
mEglHelper.finish();
mEglHelper = null;
});
}
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
if (mWorker == null) return;
mShouldStopTransition = checkIfShouldStopTransition();
mWorker.getThreadHandler().post(() -> {
mEglHelper.init(holder);
mRenderer.onSurfaceCreated();
});
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mWorker == null) return;
mWorker.getThreadHandler().post(() -> {
if (DEBUG) {
Log.d(TAG, "onSurfaceChanged: w=" + width + ", h=" + height);
}
mRenderer.onSurfaceChanged(width, height);
mNeedRedraw = true;
});
}
@Override
public void onSurfaceRedrawNeeded(SurfaceHolder holder) {
if (mWorker == null) return;
mWorker.getThreadHandler().post(() -> {
if (DEBUG) {
Log.d(TAG, "onSurfaceRedrawNeeded: mNeedRedraw=" + mNeedRedraw);
}
if (mNeedRedraw) {
drawFrame();
mNeedRedraw = false;
}
});
}
@Override
public void onVisibilityChanged(boolean visible) {
if (DEBUG) {
Log.d(TAG, "wallpaper visibility changes to: " + visible);
}
}
private void drawFrame() {
preRender();
requestRender();
postRender();
}
@Override
public void onStatePostChange() {
// When back to home, we try to release EGL, which is preserved in lock screen or aod.
if (mWorker != null && mController.getState() == StatusBarState.SHADE) {
mWorker.getThreadHandler().post(this::scheduleFinishRendering);
}
}
@Override
public void preRender() {
if (DEBUG) {
Log.d(TAG, "preRender start");
}
// This method should only be invoked from worker thread.
Trace.beginSection("ImageWallpaper#preRender");
preRenderInternal();
Trace.endSection();
if (DEBUG) {
Log.d(TAG, "preRender end");
}
}
private void preRenderInternal() {
boolean contextRecreated = false;
Rect frame = getSurfaceHolder().getSurfaceFrame();
cancelFinishRenderingTask();
// Check if we need to recreate egl context.
if (!mEglHelper.hasEglContext()) {
mEglHelper.destroyEglSurface();
if (!mEglHelper.createEglContext()) {
Log.w(TAG, "recreate egl context failed!");
} else {
contextRecreated = true;
}
}
// Check if we need to recreate egl surface.
if (mEglHelper.hasEglContext() && !mEglHelper.hasEglSurface()) {
if (!mEglHelper.createEglSurface(getSurfaceHolder())) {
Log.w(TAG, "recreate egl surface failed!");
}
}
// If we recreate egl context, notify renderer to setup again.
if (mEglHelper.hasEglContext() && mEglHelper.hasEglSurface() && contextRecreated) {
mRenderer.onSurfaceCreated();
mRenderer.onSurfaceChanged(frame.width(), frame.height());
}
}
@Override
public void requestRender() {
// This method should only be invoked from worker thread.
Trace.beginSection("ImageWallpaper#requestRender");
requestRenderInternal();
Trace.endSection();
}
private void requestRenderInternal() {
Rect frame = getSurfaceHolder().getSurfaceFrame();
boolean readyToRender = mEglHelper.hasEglContext() && mEglHelper.hasEglSurface()
&& frame.width() > 0 && frame.height() > 0;
if (readyToRender) {
mRenderer.onDrawFrame();
if (!mEglHelper.swapBuffer()) {
Log.e(TAG, "drawFrame failed!");
}
} else {
Log.e(TAG, "requestRender: not ready, has context=" + mEglHelper.hasEglContext()
+ ", has surface=" + mEglHelper.hasEglSurface()
+ ", frame=" + frame);
}
}
@Override
public void postRender() {
if (DEBUG) {
Log.d(TAG, "postRender start");
}
// This method should only be invoked from worker thread.
Trace.beginSection("ImageWallpaper#postRender");
notifyWaitingThread();
scheduleFinishRendering();
Trace.endSection();
if (DEBUG) {
Log.d(TAG, "postRender end");
}
}
private void notifyWaitingThread() {
synchronized (mMonitor) {
if (mWaitingForRendering) {
try {
mWaitingForRendering = false;
mMonitor.notify();
} catch (IllegalMonitorStateException ex) {
}
}
}
}
private void cancelFinishRenderingTask() {
if (mWorker == null) return;
mWorker.getThreadHandler().removeCallbacks(mFinishRenderingTask);
}
private void scheduleFinishRendering() {
if (mWorker == null) return;
cancelFinishRenderingTask();
mWorker.getThreadHandler().postDelayed(mFinishRenderingTask, DELAY_FINISH_RENDERING);
}
private void finishRendering() {
if (DEBUG) {
Log.d(TAG, "finishRendering, preserve=" + needPreserveEglContext());
}
Trace.beginSection("ImageWallpaper#finishRendering");
if (mEglHelper != null) {
mEglHelper.destroyEglSurface();
if (!needPreserveEglContext()) {
mEglHelper.destroyEglContext();
}
}
Trace.endSection();
}
private boolean needPreserveEglContext() {
return mNeedTransition && mController != null
&& mController.getState() == StatusBarState.KEYGUARD;
}
@Override
protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
super.dump(prefix, fd, out, args);
out.print(prefix); out.print("Engine="); out.println(this);
out.print(prefix); out.print("isHighEndGfx="); out.println(mIsHighEndGfx);
out.print(prefix); out.print("displayNeedsBlanking=");
out.println(mDisplayNeedsBlanking);
out.print(prefix); out.print("displayInfo="); out.print(mDisplayInfo);
out.print(prefix); out.print("mNeedTransition="); out.println(mNeedTransition);
out.print(prefix); out.print("mShouldStopTransition=");
out.println(mShouldStopTransition);
out.print(prefix); out.print("StatusBarState=");
out.println(mController != null ? mController.getState() : "null");
out.print(prefix); out.print("valid surface=");
out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null
? getSurfaceHolder().getSurface().isValid()
: "null");
out.print(prefix); out.print("surface frame=");
out.println(getSurfaceHolder() != null ? getSurfaceHolder().getSurfaceFrame() : "null");
mEglHelper.dump(prefix, fd, out, args);
mRenderer.dump(prefix, fd, out, args);
}
}
}