Merge "Remove GLSurfaceView and release EGL context." into qt-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
2d6f9aa60a
@@ -16,13 +16,22 @@
|
|||||||
|
|
||||||
package com.android.systemui;
|
package com.android.systemui;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.opengl.GLSurfaceView;
|
|
||||||
import android.service.wallpaper.WallpaperService;
|
import android.service.wallpaper.WallpaperService;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.Size;
|
||||||
import android.view.SurfaceHolder;
|
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.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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default built-in wallpaper that simply shows a static image.
|
* Default built-in wallpaper that simply shows a static image.
|
||||||
@@ -30,112 +39,183 @@ import com.android.systemui.glwallpaper.ImageWallpaperRenderer;
|
|||||||
@SuppressWarnings({"UnusedDeclaration"})
|
@SuppressWarnings({"UnusedDeclaration"})
|
||||||
public class ImageWallpaper extends WallpaperService {
|
public class ImageWallpaper extends WallpaperService {
|
||||||
private static final String TAG = ImageWallpaper.class.getSimpleName();
|
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;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Engine onCreateEngine() {
|
public Engine onCreateEngine() {
|
||||||
return new GLEngine(this);
|
return new GLEngine(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
class GLEngine extends Engine {
|
class GLEngine extends Engine implements GLWallpaperRenderer.SurfaceProxy, StateListener {
|
||||||
private GLWallpaperSurfaceView mWallpaperSurfaceView;
|
// 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 mNeedRedraw;
|
||||||
|
|
||||||
GLEngine(Context context) {
|
GLEngine(Context context) {
|
||||||
mWallpaperSurfaceView = new GLWallpaperSurfaceView(context);
|
mNeedTransition = ActivityManager.isHighEndGfx()
|
||||||
mWallpaperSurfaceView.setRenderer(
|
&& !DozeParameters.getInstance(context).getDisplayNeedsBlanking();
|
||||||
new ImageWallpaperRenderer(context, mWallpaperSurfaceView));
|
|
||||||
mWallpaperSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
|
// We will preserve EGL context when we are in lock screen or aod
|
||||||
setOffsetNotificationsEnabled(true);
|
// 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 */);
|
||||||
|
}
|
||||||
|
mEglHelper = new EglHelper();
|
||||||
|
mRenderer = new ImageWallpaperRenderer(context, this /* SurfaceProxy */);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SurfaceHolder surfaceHolder) {
|
||||||
|
setFixedSizeAllowed(true);
|
||||||
|
setOffsetNotificationsEnabled(false);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) {
|
public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) {
|
||||||
if (mWallpaperSurfaceView != null) {
|
mRenderer.updateAmbientMode(inAmbientMode,
|
||||||
mWallpaperSurfaceView.notifyAmbientModeChanged(inAmbientMode, animationDuration);
|
(mNeedTransition || animationDuration != 0) ? animationDuration : 0);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep,
|
|
||||||
float yOffsetStep, int xPixelOffset, int yPixelOffset) {
|
|
||||||
if (mWallpaperSurfaceView != null) {
|
|
||||||
mWallpaperSurfaceView.notifyOffsetsChanged(xOffset, yOffset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
if (mWallpaperSurfaceView != null) {
|
if (mController != null) {
|
||||||
mWallpaperSurfaceView.onPause();
|
mController.removeCallback(this /* StateListener */);
|
||||||
}
|
}
|
||||||
|
mController = null;
|
||||||
|
mRenderer.finish();
|
||||||
|
mRenderer = null;
|
||||||
|
mEglHelper.finish();
|
||||||
|
mEglHelper = null;
|
||||||
|
getSurfaceHolder().getSurface().hwuiDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class GLWallpaperSurfaceView extends GLSurfaceView implements ImageGLView {
|
@Override
|
||||||
private WallpaperStatusListener mWallpaperStatusListener;
|
public void onSurfaceCreated(SurfaceHolder holder) {
|
||||||
|
mEglHelper.init(holder);
|
||||||
|
mRenderer.onSurfaceCreated();
|
||||||
|
}
|
||||||
|
|
||||||
GLWallpaperSurfaceView(Context context) {
|
@Override
|
||||||
super(context);
|
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||||
setEGLContextClientVersion(2);
|
mRenderer.onSurfaceChanged(width, height);
|
||||||
}
|
mNeedRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SurfaceHolder getHolder() {
|
public void onSurfaceRedrawNeeded(SurfaceHolder holder) {
|
||||||
return getSurfaceHolder();
|
if (mNeedRedraw) {
|
||||||
}
|
preRender();
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setRenderer(Renderer renderer) {
|
|
||||||
super.setRenderer(renderer);
|
|
||||||
mWallpaperStatusListener = (WallpaperStatusListener) renderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyAmbientModeChanged(boolean inAmbient, long duration) {
|
|
||||||
if (mWallpaperStatusListener != null) {
|
|
||||||
mWallpaperStatusListener.onAmbientModeChanged(inAmbient, duration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOffsetsChanged(float xOffset, float yOffset) {
|
|
||||||
if (mWallpaperStatusListener != null) {
|
|
||||||
mWallpaperStatusListener.onOffsetsChanged(
|
|
||||||
xOffset, yOffset, getHolder().getSurfaceFrame());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void render() {
|
|
||||||
requestRender();
|
requestRender();
|
||||||
|
postRender();
|
||||||
|
mNeedRedraw = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* A listener to trace status of image wallpaper.
|
public SurfaceHolder getHolder() {
|
||||||
*/
|
return getSurfaceHolder();
|
||||||
public interface WallpaperStatusListener {
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Called back while ambient mode changes.
|
public void onStatePostChange() {
|
||||||
* @param inAmbientMode true if is in ambient mode, false otherwise.
|
// When back to home, we try to release EGL, which is preserved in lock screen or aod.
|
||||||
* @param duration the duration of animation.
|
if (mController.getState() == StatusBarState.SHADE) {
|
||||||
*/
|
scheduleFinishRendering();
|
||||||
void onAmbientModeChanged(boolean inAmbientMode, long duration);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Called back while wallpaper offsets.
|
public void preRender() {
|
||||||
* @param xOffset The offset portion along x.
|
boolean contextRecreated = false;
|
||||||
* @param yOffset The offset portion along y.
|
Rect frame = getSurfaceHolder().getSurfaceFrame();
|
||||||
*/
|
getMainThreadHandler().removeCallbacks(mFinishRenderingTask);
|
||||||
void onOffsetsChanged(float xOffset, float yOffset, Rect frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Check if we need to recreate egl context.
|
||||||
* An abstraction for view of GLRenderer.
|
if (!mEglHelper.hasEglContext()) {
|
||||||
*/
|
mEglHelper.destroyEglSurface();
|
||||||
public interface ImageGLView {
|
if (!mEglHelper.createEglContext()) {
|
||||||
|
Log.w(TAG, "recreate egl context failed!");
|
||||||
|
} else {
|
||||||
|
contextRecreated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
// Check if we need to recreate egl surface.
|
||||||
* Ask the view to render.
|
if (mEglHelper.hasEglContext() && !mEglHelper.hasEglSurface()) {
|
||||||
*/
|
if (!mEglHelper.createEglSurface(getSurfaceHolder())) {
|
||||||
void render();
|
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() {
|
||||||
|
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() {
|
||||||
|
scheduleFinishRendering();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleFinishRendering() {
|
||||||
|
getMainThreadHandler().removeCallbacks(mFinishRenderingTask);
|
||||||
|
getMainThreadHandler().postDelayed(mFinishRenderingTask, DELAY_FINISH_RENDERING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishRendering() {
|
||||||
|
if (mEglHelper != null) {
|
||||||
|
mEglHelper.destroyEglSurface();
|
||||||
|
if (!needPreserveEglContext()) {
|
||||||
|
mEglHelper.destroyEglContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean needPreserveEglContext() {
|
||||||
|
return mNeedTransition && mController != null
|
||||||
|
&& mController.getState() == StatusBarState.KEYGUARD;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 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.glwallpaper;
|
||||||
|
|
||||||
|
import static android.opengl.EGL14.EGL_ALPHA_SIZE;
|
||||||
|
import static android.opengl.EGL14.EGL_BLUE_SIZE;
|
||||||
|
import static android.opengl.EGL14.EGL_CONFIG_CAVEAT;
|
||||||
|
import static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION;
|
||||||
|
import static android.opengl.EGL14.EGL_DEFAULT_DISPLAY;
|
||||||
|
import static android.opengl.EGL14.EGL_DEPTH_SIZE;
|
||||||
|
import static android.opengl.EGL14.EGL_GREEN_SIZE;
|
||||||
|
import static android.opengl.EGL14.EGL_NONE;
|
||||||
|
import static android.opengl.EGL14.EGL_NO_CONTEXT;
|
||||||
|
import static android.opengl.EGL14.EGL_NO_DISPLAY;
|
||||||
|
import static android.opengl.EGL14.EGL_NO_SURFACE;
|
||||||
|
import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT;
|
||||||
|
import static android.opengl.EGL14.EGL_RED_SIZE;
|
||||||
|
import static android.opengl.EGL14.EGL_RENDERABLE_TYPE;
|
||||||
|
import static android.opengl.EGL14.EGL_STENCIL_SIZE;
|
||||||
|
import static android.opengl.EGL14.EGL_SUCCESS;
|
||||||
|
import static android.opengl.EGL14.eglChooseConfig;
|
||||||
|
import static android.opengl.EGL14.eglCreateContext;
|
||||||
|
import static android.opengl.EGL14.eglCreateWindowSurface;
|
||||||
|
import static android.opengl.EGL14.eglDestroyContext;
|
||||||
|
import static android.opengl.EGL14.eglDestroySurface;
|
||||||
|
import static android.opengl.EGL14.eglGetDisplay;
|
||||||
|
import static android.opengl.EGL14.eglGetError;
|
||||||
|
import static android.opengl.EGL14.eglInitialize;
|
||||||
|
import static android.opengl.EGL14.eglMakeCurrent;
|
||||||
|
import static android.opengl.EGL14.eglSwapBuffers;
|
||||||
|
import static android.opengl.EGL14.eglTerminate;
|
||||||
|
|
||||||
|
import android.opengl.EGLConfig;
|
||||||
|
import android.opengl.EGLContext;
|
||||||
|
import android.opengl.EGLDisplay;
|
||||||
|
import android.opengl.EGLSurface;
|
||||||
|
import android.opengl.GLUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class to handle EGL management.
|
||||||
|
*/
|
||||||
|
public class EglHelper {
|
||||||
|
private static final String TAG = EglHelper.class.getSimpleName();
|
||||||
|
|
||||||
|
private EGLDisplay mEglDisplay;
|
||||||
|
private EGLConfig mEglConfig;
|
||||||
|
private EGLContext mEglContext;
|
||||||
|
private EGLSurface mEglSurface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize EGL and prepare EglSurface.
|
||||||
|
* @param surfaceHolder surface holder.
|
||||||
|
* @return true if EglSurface is ready.
|
||||||
|
*/
|
||||||
|
public boolean init(SurfaceHolder surfaceHolder) {
|
||||||
|
mEglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
|
if (mEglDisplay == EGL_NO_DISPLAY) {
|
||||||
|
Log.w(TAG, "eglGetDisplay failed: " + GLUtils.getEGLErrorString(eglGetError()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eglInitialize(mEglDisplay, null, 0, null, 0)) {
|
||||||
|
Log.w(TAG, "eglInitialize failed: " + GLUtils.getEGLErrorString(eglGetError()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mEglConfig = chooseEglConfig();
|
||||||
|
if (mEglConfig == null) {
|
||||||
|
Log.w(TAG, "eglConfig not initialized!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createEglContext()) {
|
||||||
|
Log.w(TAG, "Can't create EGLContext!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createEglSurface(surfaceHolder)) {
|
||||||
|
Log.w(TAG, "Can't create EGLSurface!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EGLConfig chooseEglConfig() {
|
||||||
|
int[] configsCount = new int[1];
|
||||||
|
EGLConfig[] configs = new EGLConfig[1];
|
||||||
|
int[] configSpec = getConfig();
|
||||||
|
if (!eglChooseConfig(mEglDisplay, configSpec, 0, configs, 0, 1, configsCount, 0)) {
|
||||||
|
Log.w(TAG, "eglChooseConfig failed: " + GLUtils.getEGLErrorString(eglGetError()));
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
if (configsCount[0] <= 0) {
|
||||||
|
Log.w(TAG, "eglChooseConfig failed, invalid configs count: " + configsCount[0]);
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return configs[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] getConfig() {
|
||||||
|
return new int[] {
|
||||||
|
EGL_RED_SIZE, 8,
|
||||||
|
EGL_GREEN_SIZE, 8,
|
||||||
|
EGL_BLUE_SIZE, 8,
|
||||||
|
EGL_ALPHA_SIZE, 8,
|
||||||
|
EGL_DEPTH_SIZE, 0,
|
||||||
|
EGL_STENCIL_SIZE, 0,
|
||||||
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||||
|
EGL_CONFIG_CAVEAT, EGL_NONE,
|
||||||
|
EGL_NONE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare an EglSurface.
|
||||||
|
* @param surfaceHolder surface holder.
|
||||||
|
* @return true if EglSurface is ready.
|
||||||
|
*/
|
||||||
|
public boolean createEglSurface(SurfaceHolder surfaceHolder) {
|
||||||
|
mEglSurface = eglCreateWindowSurface(mEglDisplay, mEglConfig, surfaceHolder, null, 0);
|
||||||
|
if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) {
|
||||||
|
Log.w(TAG, "createWindowSurface failed: " + GLUtils.getEGLErrorString(eglGetError()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
|
||||||
|
Log.w(TAG, "eglMakeCurrent failed: " + GLUtils.getEGLErrorString(eglGetError()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy EglSurface.
|
||||||
|
*/
|
||||||
|
public void destroyEglSurface() {
|
||||||
|
if (hasEglSurface()) {
|
||||||
|
eglMakeCurrent(mEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
|
eglDestroySurface(mEglDisplay, mEglSurface);
|
||||||
|
mEglSurface = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we have a valid EglSurface.
|
||||||
|
* @return true if EglSurface is ready.
|
||||||
|
*/
|
||||||
|
public boolean hasEglSurface() {
|
||||||
|
return mEglSurface != null && mEglSurface != EGL_NO_SURFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare EglContext.
|
||||||
|
* @return true if EglContext is ready.
|
||||||
|
*/
|
||||||
|
public boolean createEglContext() {
|
||||||
|
int[] attrib_list = new int[] {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
|
||||||
|
mEglContext = eglCreateContext(mEglDisplay, mEglConfig, EGL_NO_CONTEXT, attrib_list, 0);
|
||||||
|
if (mEglContext == EGL_NO_CONTEXT) {
|
||||||
|
Log.w(TAG, "eglCreateContext failed: " + GLUtils.getEGLErrorString(eglGetError()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy EglContext.
|
||||||
|
*/
|
||||||
|
public void destroyEglContext() {
|
||||||
|
if (hasEglContext()) {
|
||||||
|
eglDestroyContext(mEglDisplay, mEglContext);
|
||||||
|
mEglContext = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we have EglContext.
|
||||||
|
* @return true if EglContext is ready.
|
||||||
|
*/
|
||||||
|
public boolean hasEglContext() {
|
||||||
|
return mEglContext != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swap buffer to display.
|
||||||
|
* @return true if swap successfully.
|
||||||
|
*/
|
||||||
|
public boolean swapBuffer() {
|
||||||
|
boolean status = eglSwapBuffers(mEglDisplay, mEglSurface);
|
||||||
|
int error = eglGetError();
|
||||||
|
if (error != EGL_SUCCESS) {
|
||||||
|
Log.w(TAG, "eglSwapBuffers failed: " + GLUtils.getEGLErrorString(error));
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy EglSurface and EglContext, then terminate EGL.
|
||||||
|
*/
|
||||||
|
public void finish() {
|
||||||
|
if (hasEglSurface()) {
|
||||||
|
destroyEglSurface();
|
||||||
|
}
|
||||||
|
if (hasEglContext()) {
|
||||||
|
destroyEglContext();
|
||||||
|
}
|
||||||
|
eglTerminate(mEglDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 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.glwallpaper;
|
||||||
|
|
||||||
|
import android.util.Size;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A renderer which is responsible for making OpenGL calls to render a frame.
|
||||||
|
*/
|
||||||
|
public interface GLWallpaperRenderer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the surface is created or recreated.
|
||||||
|
*/
|
||||||
|
void onSurfaceCreated();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the surface changed size.
|
||||||
|
* @param width surface width.
|
||||||
|
* @param height surface height.
|
||||||
|
*/
|
||||||
|
void onSurfaceChanged(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to draw the current frame.
|
||||||
|
*/
|
||||||
|
void onDrawFrame();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify ambient mode is changed.
|
||||||
|
* @param inAmbientMode true if in ambient mode.
|
||||||
|
* @param duration duration of transition.
|
||||||
|
*/
|
||||||
|
void updateAmbientMode(boolean inAmbientMode, long duration);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask renderer to report the surface size it needs.
|
||||||
|
*/
|
||||||
|
Size reportSurfaceSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when no need to render any more.
|
||||||
|
*/
|
||||||
|
void finish();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A proxy which owns surface holder.
|
||||||
|
*/
|
||||||
|
interface SurfaceProxy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get surface holder.
|
||||||
|
* @return surface holder.
|
||||||
|
*/
|
||||||
|
SurfaceHolder getHolder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask proxy to start rendering frame to surface.
|
||||||
|
*/
|
||||||
|
void requestRender();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask proxy to prepare render context.
|
||||||
|
*/
|
||||||
|
void preRender();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask proxy to destroy render context.
|
||||||
|
*/
|
||||||
|
void postRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -195,76 +195,4 @@ class ImageGLWallpaper {
|
|||||||
glUniform1i(mUniTexture, 0);
|
glUniform1i(mUniTexture, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method adjust s(x-axis), t(y-axis) texture coordinates
|
|
||||||
* to prevent the wallpaper from being stretched.
|
|
||||||
* The adjustment happens if either the width or height of the bitmap is larger than
|
|
||||||
* corresponding size of the surface.
|
|
||||||
* If both width and height are larger than corresponding size of the surface,
|
|
||||||
* the adjustment will happen at both s, t side.
|
|
||||||
*
|
|
||||||
* @param bitmapWidth The width of the bitmap.
|
|
||||||
* @param bitmapHeight The height of the bitmap.
|
|
||||||
* @param surfaceWidth The width of the surface.
|
|
||||||
* @param surfaceHeight The height of the surface.
|
|
||||||
* @param xOffset The offset amount along s axis.
|
|
||||||
* @param yOffset The offset amount along t axis.
|
|
||||||
*/
|
|
||||||
void adjustTextureCoordinates(int bitmapWidth, int bitmapHeight,
|
|
||||||
int surfaceWidth, int surfaceHeight, float xOffset, float yOffset) {
|
|
||||||
float[] coordinates = TEXTURES.clone();
|
|
||||||
|
|
||||||
if (bitmapWidth > surfaceWidth) {
|
|
||||||
// Calculate the new s pos in pixels.
|
|
||||||
float pixelS = (float) Math.round((bitmapWidth - surfaceWidth) * xOffset);
|
|
||||||
// Calculate the s pos in texture coordinate.
|
|
||||||
float coordinateS = pixelS / bitmapWidth;
|
|
||||||
// Calculate the percentage occupied by the surface width in bitmap width.
|
|
||||||
float surfacePercentageW = (float) surfaceWidth / bitmapWidth;
|
|
||||||
// Need also consider the case if bitmap height is smaller than surface height.
|
|
||||||
if (bitmapHeight < surfaceHeight) {
|
|
||||||
// We will narrow the surface percentage to keep aspect ratio.
|
|
||||||
surfacePercentageW *= (float) bitmapHeight / surfaceHeight;
|
|
||||||
}
|
|
||||||
// Determine the final s pos, also limit the legal s pos to prevent from out of range.
|
|
||||||
float s = coordinateS + surfacePercentageW > 1f ? 1f - surfacePercentageW : coordinateS;
|
|
||||||
// Traverse the s pos in texture coordinates array and adjust the s pos accordingly.
|
|
||||||
for (int i = 0; i < coordinates.length; i += 2) {
|
|
||||||
// indices 2, 4 and 6 are the end of s coordinates.
|
|
||||||
if (i == 2 || i == 4 || i == 6) {
|
|
||||||
coordinates[i] = Math.min(1f, s + surfacePercentageW);
|
|
||||||
} else {
|
|
||||||
coordinates[i] = s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bitmapHeight > surfaceHeight) {
|
|
||||||
// Calculate the new t pos in pixels.
|
|
||||||
float pixelT = (float) Math.round((bitmapHeight - surfaceHeight) * yOffset);
|
|
||||||
// Calculate the t pos in texture coordinate.
|
|
||||||
float coordinateT = pixelT / bitmapHeight;
|
|
||||||
// Calculate the percentage occupied by the surface height in bitmap height.
|
|
||||||
float surfacePercentageH = (float) surfaceHeight / bitmapHeight;
|
|
||||||
// Need also consider the case if bitmap width is smaller than surface width.
|
|
||||||
if (bitmapWidth < surfaceWidth) {
|
|
||||||
// We will narrow the surface percentage to keep aspect ratio.
|
|
||||||
surfacePercentageH *= (float) bitmapWidth / surfaceWidth;
|
|
||||||
}
|
|
||||||
// Determine the final t pos, also limit the legal t pos to prevent from out of range.
|
|
||||||
float t = coordinateT + surfacePercentageH > 1f ? 1f - surfacePercentageH : coordinateT;
|
|
||||||
// Traverse the t pos in texture coordinates array and adjust the t pos accordingly.
|
|
||||||
for (int i = 1; i < coordinates.length; i += 2) {
|
|
||||||
// indices 1, 3 and 11 are the end of t coordinates.
|
|
||||||
if (i == 1 || i == 3 || i == 11) {
|
|
||||||
coordinates[i] = Math.min(1f, t + surfacePercentageH);
|
|
||||||
} else {
|
|
||||||
coordinates[i] = t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mTextureBuffer.put(coordinates);
|
|
||||||
mTextureBuffer.position(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,11 +56,18 @@ class ImageRevealHelper {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(Animator animation) {
|
public void onAnimationEnd(Animator animation) {
|
||||||
if (!mIsCanceled) {
|
if (!mIsCanceled && mRevealListener != null) {
|
||||||
mAwake = !mAwake;
|
mRevealListener.onRevealEnd();
|
||||||
}
|
}
|
||||||
mIsCanceled = false;
|
mIsCanceled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animation) {
|
||||||
|
if (mRevealListener != null) {
|
||||||
|
mRevealListener.onRevealStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,10 +81,6 @@ class ImageRevealHelper {
|
|||||||
return mReveal;
|
return mReveal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAwake() {
|
|
||||||
return mAwake;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateAwake(boolean awake, long duration) {
|
void updateAwake(boolean awake, long duration) {
|
||||||
mAwake = awake;
|
mAwake = awake;
|
||||||
mAnimator.setDuration(duration);
|
mAnimator.setDuration(duration);
|
||||||
@@ -93,5 +96,15 @@ class ImageRevealHelper {
|
|||||||
* Called back while reveal status changes.
|
* Called back while reveal status changes.
|
||||||
*/
|
*/
|
||||||
void onRevealStateChanged();
|
void onRevealStateChanged();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called back while reveal starts.
|
||||||
|
*/
|
||||||
|
void onRevealStart();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called back while reveal ends.
|
||||||
|
*/
|
||||||
|
void onRevealEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,23 +26,17 @@ import android.app.WallpaperManager;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.opengl.GLSurfaceView;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.MathUtils;
|
import android.util.MathUtils;
|
||||||
|
import android.util.Size;
|
||||||
|
|
||||||
import com.android.systemui.ImageWallpaper;
|
|
||||||
import com.android.systemui.ImageWallpaper.ImageGLView;
|
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig;
|
|
||||||
import javax.microedition.khronos.opengles.GL10;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A GL renderer for image wallpaper.
|
* A GL renderer for image wallpaper.
|
||||||
*/
|
*/
|
||||||
public class ImageWallpaperRenderer implements GLSurfaceView.Renderer,
|
public class ImageWallpaperRenderer implements GLWallpaperRenderer,
|
||||||
ImageWallpaper.WallpaperStatusListener, ImageRevealHelper.RevealStateListener {
|
ImageRevealHelper.RevealStateListener {
|
||||||
private static final String TAG = ImageWallpaperRenderer.class.getSimpleName();
|
private static final String TAG = ImageWallpaperRenderer.class.getSimpleName();
|
||||||
private static final float SCALE_VIEWPORT_MIN = 0.98f;
|
private static final float SCALE_VIEWPORT_MIN = 0.98f;
|
||||||
private static final float SCALE_VIEWPORT_MAX = 1f;
|
private static final float SCALE_VIEWPORT_MAX = 1f;
|
||||||
@@ -52,62 +46,61 @@ public class ImageWallpaperRenderer implements GLSurfaceView.Renderer,
|
|||||||
private final ImageGLWallpaper mWallpaper;
|
private final ImageGLWallpaper mWallpaper;
|
||||||
private final ImageProcessHelper mImageProcessHelper;
|
private final ImageProcessHelper mImageProcessHelper;
|
||||||
private final ImageRevealHelper mImageRevealHelper;
|
private final ImageRevealHelper mImageRevealHelper;
|
||||||
private final ImageGLView mGLView;
|
|
||||||
private float mXOffset = 0f;
|
|
||||||
private float mYOffset = 0f;
|
|
||||||
private int mWidth = 0;
|
|
||||||
private int mHeight = 0;
|
|
||||||
|
|
||||||
|
private SurfaceProxy mProxy;
|
||||||
|
private Rect mSurfaceSize;
|
||||||
private Bitmap mBitmap;
|
private Bitmap mBitmap;
|
||||||
private int mBitmapWidth = 0;
|
|
||||||
private int mBitmapHeight = 0;
|
|
||||||
|
|
||||||
public ImageWallpaperRenderer(Context context, ImageGLView glView) {
|
public ImageWallpaperRenderer(Context context, SurfaceProxy proxy) {
|
||||||
mWallpaperManager = context.getSystemService(WallpaperManager.class);
|
mWallpaperManager = context.getSystemService(WallpaperManager.class);
|
||||||
if (mWallpaperManager == null) {
|
if (mWallpaperManager == null) {
|
||||||
Log.w(TAG, "WallpaperManager not available");
|
Log.w(TAG, "WallpaperManager not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mProxy = proxy;
|
||||||
mProgram = new ImageGLProgram(context);
|
mProgram = new ImageGLProgram(context);
|
||||||
mWallpaper = new ImageGLWallpaper(mProgram);
|
mWallpaper = new ImageGLWallpaper(mProgram);
|
||||||
mImageProcessHelper = new ImageProcessHelper();
|
mImageProcessHelper = new ImageProcessHelper();
|
||||||
mImageRevealHelper = new ImageRevealHelper(this);
|
mImageRevealHelper = new ImageRevealHelper(this);
|
||||||
mGLView = glView;
|
|
||||||
|
|
||||||
if (mWallpaperManager != null) {
|
if (loadBitmap()) {
|
||||||
mBitmap = mWallpaperManager.getBitmap();
|
|
||||||
mBitmapWidth = mBitmap.getWidth();
|
|
||||||
mBitmapHeight = mBitmap.getHeight();
|
|
||||||
// Compute threshold of the image, this is an async work.
|
// Compute threshold of the image, this is an async work.
|
||||||
mImageProcessHelper.start(mBitmap);
|
mImageProcessHelper.start(mBitmap);
|
||||||
mWallpaperManager.forgetLoadedWallpaper();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
public void onSurfaceCreated() {
|
||||||
glClearColor(0f, 0f, 0f, 1.0f);
|
glClearColor(0f, 0f, 0f, 1.0f);
|
||||||
mProgram.useGLProgram(
|
mProgram.useGLProgram(
|
||||||
R.raw.image_wallpaper_vertex_shader, R.raw.image_wallpaper_fragment_shader);
|
R.raw.image_wallpaper_vertex_shader, R.raw.image_wallpaper_fragment_shader);
|
||||||
|
|
||||||
|
if (!loadBitmap()) {
|
||||||
|
Log.w(TAG, "reload bitmap failed!");
|
||||||
|
}
|
||||||
|
|
||||||
mWallpaper.setup(mBitmap);
|
mWallpaper.setup(mBitmap);
|
||||||
mBitmap = null;
|
mBitmap = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean loadBitmap() {
|
||||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
if (mWallpaperManager != null && mBitmap == null) {
|
||||||
glViewport(0, 0, width, height);
|
mBitmap = mWallpaperManager.getBitmap();
|
||||||
if (Build.IS_DEBUGGABLE) {
|
mWallpaperManager.forgetLoadedWallpaper();
|
||||||
Log.d(TAG, "onSurfaceChanged: width=" + width + ", height=" + height
|
if (mBitmap != null) {
|
||||||
+ ", xOffset=" + mXOffset + ", yOffset=" + mYOffset);
|
mSurfaceSize = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mWidth = width;
|
return mBitmap != null;
|
||||||
mHeight = height;
|
|
||||||
mWallpaper.adjustTextureCoordinates(
|
|
||||||
mBitmapWidth, mBitmapHeight, width, height, mXOffset, mYOffset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDrawFrame(GL10 gl) {
|
public void onSurfaceChanged(int width, int height) {
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrawFrame() {
|
||||||
float threshold = mImageProcessHelper.getThreshold();
|
float threshold = mImageProcessHelper.getThreshold();
|
||||||
float reveal = mImageRevealHelper.getReveal();
|
float reveal = mImageRevealHelper.getReveal();
|
||||||
|
|
||||||
@@ -122,50 +115,45 @@ public class ImageWallpaperRenderer implements GLSurfaceView.Renderer,
|
|||||||
mWallpaper.draw();
|
mWallpaper.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateAmbientMode(boolean inAmbientMode, long duration) {
|
||||||
|
mImageRevealHelper.updateAwake(!inAmbientMode, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Size reportSurfaceSize() {
|
||||||
|
return new Size(mSurfaceSize.width(), mSurfaceSize.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
mProxy = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void scaleViewport(float reveal) {
|
private void scaleViewport(float reveal) {
|
||||||
|
int width = mSurfaceSize.width();
|
||||||
|
int height = mSurfaceSize.height();
|
||||||
// Interpolation between SCALE_VIEWPORT_MAX and SCALE_VIEWPORT_MIN by reveal.
|
// Interpolation between SCALE_VIEWPORT_MAX and SCALE_VIEWPORT_MIN by reveal.
|
||||||
float vpScaled = MathUtils.lerp(SCALE_VIEWPORT_MAX, SCALE_VIEWPORT_MIN, reveal);
|
float vpScaled = MathUtils.lerp(SCALE_VIEWPORT_MAX, SCALE_VIEWPORT_MIN, reveal);
|
||||||
// Calculate the offset amount from the lower left corner.
|
// Calculate the offset amount from the lower left corner.
|
||||||
float offset = (SCALE_VIEWPORT_MAX - vpScaled) / 2;
|
float offset = (SCALE_VIEWPORT_MAX - vpScaled) / 2;
|
||||||
// Change the viewport.
|
// Change the viewport.
|
||||||
glViewport((int) (mWidth * offset), (int) (mHeight * offset),
|
glViewport((int) (width * offset), (int) (height * offset),
|
||||||
(int) (mWidth * vpScaled), (int) (mHeight * vpScaled));
|
(int) (width * vpScaled), (int) (height * vpScaled));
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAmbientModeChanged(boolean inAmbientMode, long duration) {
|
|
||||||
mImageRevealHelper.updateAwake(!inAmbientMode, duration);
|
|
||||||
requestRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOffsetsChanged(float xOffset, float yOffset, Rect frame) {
|
|
||||||
if (frame == null || (xOffset == mXOffset && yOffset == mYOffset)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int width = frame.width();
|
|
||||||
int height = frame.height();
|
|
||||||
mXOffset = xOffset;
|
|
||||||
mYOffset = yOffset;
|
|
||||||
|
|
||||||
if (Build.IS_DEBUGGABLE) {
|
|
||||||
Log.d(TAG, "onOffsetsChanged: width=" + width + ", height=" + height
|
|
||||||
+ ", xOffset=" + mXOffset + ", yOffset=" + mYOffset);
|
|
||||||
}
|
|
||||||
mWallpaper.adjustTextureCoordinates(
|
|
||||||
mBitmapWidth, mBitmapHeight, width, height, mXOffset, mYOffset);
|
|
||||||
requestRender();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRevealStateChanged() {
|
public void onRevealStateChanged() {
|
||||||
requestRender();
|
mProxy.requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestRender() {
|
@Override
|
||||||
if (mGLView != null) {
|
public void onRevealStart() {
|
||||||
mGLView.render();
|
mProxy.preRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevealEnd() {
|
||||||
|
mProxy.postRender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user