/* * 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 android.service.wallpaper; import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WallpaperManager.SetWallpaperFlags; import static android.graphics.Matrix.MSCALE_X; import static android.graphics.Matrix.MSCALE_Y; import static android.graphics.Matrix.MSKEW_X; import static android.graphics.Matrix.MSKEW_Y; import static android.view.View.SYSTEM_UI_FLAG_VISIBLE; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static com.android.window.flags.Flags.FLAG_OFFLOAD_COLOR_EXTRACTION; import static com.android.window.flags.Flags.noConsecutiveVisibilityEvents; import static com.android.window.flags.Flags.offloadColorExtraction; import static com.android.window.flags.Flags.windowSessionRelayoutInfo; import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; import android.app.Service; import android.app.WallpaperColors; import android.app.WallpaperInfo; import android.app.WallpaperManager; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.BLASTBufferQueue; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.hardware.HardwareBuffer; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.MergedConfiguration; import android.util.Slog; import android.view.Display; import android.view.DisplayCutout; import android.view.Gravity; import android.view.IWindowSession; import android.view.InputChannel; import android.view.InputDevice; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.MotionEvent; import android.view.PixelCopy; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowLayout; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowRelayoutResult; import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.ScreenCapture; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.HandlerCaller; import com.android.internal.view.BaseIWindow; import com.android.internal.view.BaseSurfaceHolder; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; /** * A wallpaper service is responsible for showing a live wallpaper behind * applications that would like to sit on top of it. This service object * itself does very little -- its only purpose is to generate instances of * {@link Engine} as needed. Implementing a wallpaper thus * involves subclassing from this, subclassing an Engine implementation, * and implementing {@link #onCreateEngine()} to return a new instance of * your engine. */ public abstract class WallpaperService extends Service { /** * The {@link Intent} that must be declared as handled by the service. * To be supported, the service must also require the * {@link android.Manifest.permission#BIND_WALLPAPER} permission so * that other applications can not abuse it. */ @SdkConstant(SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = "android.service.wallpaper.WallpaperService"; /** * Name under which a WallpaperService component publishes information * about itself. This meta-data must reference an XML resource containing * a <{@link android.R.styleable#Wallpaper wallpaper}> * tag. */ public static final String SERVICE_META_DATA = "android.service.wallpaper"; static final String TAG = "WallpaperService"; static final boolean DEBUG = false; static final float MIN_PAGE_ALLOWED_MARGIN = .05f; private static final int MIN_BITMAP_SCREENSHOT_WIDTH = 64; private static final long PRESERVE_VISIBLE_TIMEOUT_MS = 1000; private static final long DEFAULT_UPDATE_SCREENSHOT_DURATION = 60 * 1000; //Once per minute private static final @NonNull RectF LOCAL_COLOR_BOUNDS = new RectF(0, 0, 1, 1); private static final int DO_ATTACH = 10; private static final int DO_DETACH = 20; private static final int DO_SET_DESIRED_SIZE = 30; private static final int DO_SET_DISPLAY_PADDING = 40; private static final int DO_IN_AMBIENT_MODE = 50; private static final int MSG_UPDATE_SURFACE = 10000; private static final int MSG_VISIBILITY_CHANGED = 10010; private static final int MSG_REFRESH_VISIBILITY = 10011; private static final int MSG_WALLPAPER_OFFSETS = 10020; private static final int MSG_WALLPAPER_COMMAND = 10025; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private static final int MSG_WINDOW_RESIZED = 10030; private static final int MSG_WINDOW_MOVED = 10035; private static final int MSG_TOUCH_EVENT = 10040; private static final int MSG_REQUEST_WALLPAPER_COLORS = 10050; private static final int MSG_ZOOM = 10100; private static final int MSG_RESIZE_PREVIEW = 10110; private static final int MSG_REPORT_SHOWN = 10150; private static final int MSG_UPDATE_SCREEN_TURNING_ON = 10170; private static final int MSG_UPDATE_DIMMING = 10200; private static final int MSG_WALLPAPER_FLAGS_CHANGED = 10210; /** limit calls to {@link Engine#onComputeColors} to at most once per second */ private static final int NOTIFY_COLORS_RATE_LIMIT_MS = 1000; /** limit calls to {@link Engine#processLocalColorsInternal} to at most once per 2 seconds */ private static final int PROCESS_LOCAL_COLORS_INTERVAL_MS = 2000; private static final boolean ENABLE_WALLPAPER_DIMMING = SystemProperties.getBoolean("persist.debug.enable_wallpaper_dimming", true); private static final long DIMMING_ANIMATION_DURATION_MS = 300L; @GuardedBy("itself") private final ArrayMap mActiveEngines = new ArrayMap<>(); private Handler mBackgroundHandler; private HandlerThread mBackgroundThread; // TODO (b/287037772) remove this flag and the forceReport argument in reportVisibility private boolean mIsWearOs; /** * Wear products currently force a slight scaling transition to wallpapers * when the QSS is opened. However, on Wear 6 (SDK 35) and above, 1P watch faces * will be expected to either implement their own scaling, or to override this * method to allow the WallpaperController to continue to scale for them. * * @hide */ @ChangeId @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L; static final class WallpaperCommand { String action; int x; int y; int z; Bundle extras; boolean sync; } /** * The actual implementation of a wallpaper. A wallpaper service may * have multiple instances running (for example as a real wallpaper * and as a preview), each of which is represented by its own Engine * instance. You must implement {@link WallpaperService#onCreateEngine()} * to return your concrete Engine implementation. */ public class Engine { IWallpaperEngineWrapper mIWallpaperEngine; // Copies from mIWallpaperEngine. HandlerCaller mCaller; IWallpaperConnection mConnection; IBinder mWindowToken; boolean mInitializing = true; boolean mVisible; /** * Whether the screen is turning on. * After the display is powered on, brightness is initially off. It is turned on only after * all windows have been drawn, and sysui notifies that it's ready (See * {@link com.android.internal.policy.IKeyguardDrawnCallback}). * As some wallpapers use visibility as a signal to start animations, this makes sure * {@link Engine#onVisibilityChanged} is invoked only when the display is both on and * visible (with brightness on). */ private boolean mIsScreenTurningOn; boolean mReportedVisible; /** * This is used with {@link #PRESERVE_VISIBLE_TIMEOUT_MS} to avoid intermediate visibility * changes if the display may be toggled in a short time, e.g. display switch. */ boolean mPreserveVisible; boolean mDestroyed; // Set to true after receiving WallpaperManager#COMMAND_FREEZE. It's reset back to false // after receiving WallpaperManager#COMMAND_UNFREEZE. COMMAND_FREEZE is fully applied once // mScreenshotSurfaceControl isn't null. When this happens, then Engine is notified through // doVisibilityChanged that main wallpaper surface is no longer visible and the wallpaper // host receives onVisibilityChanged(false) callback. private boolean mFrozenRequested = false; // Current window state. boolean mCreated; boolean mSurfaceCreated; boolean mIsCreating; boolean mDrawingAllowed; boolean mOffsetsChanged; boolean mFixedSizeAllowed; // Whether the wallpaper should be dimmed by default (when no additional dimming is applied) // based on its color hints boolean mShouldDimByDefault; int mWidth; int mHeight; int mFormat; int mType; int mCurWidth; int mCurHeight; float mZoom = 0f; int mWindowFlags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; int mWindowPrivateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS; int mCurWindowFlags = mWindowFlags; int mCurWindowPrivateFlags = mWindowPrivateFlags; Rect mPreviewSurfacePosition; final ClientWindowFrames mWinFrames = new ClientWindowFrames(); final Rect mDispatchedContentInsets = new Rect(); final Rect mDispatchedStableInsets = new Rect(); DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT; final InsetsState mInsetsState = new InsetsState(); final InsetsSourceControl.Array mTempControls = new InsetsSourceControl.Array(); final MergedConfiguration mMergedConfiguration = new MergedConfiguration(); final Bundle mSyncSeqIdBundle = windowSessionRelayoutInfo() ? null : new Bundle(); SurfaceControl mSurfaceControl = new SurfaceControl(); WindowRelayoutResult mRelayoutResult = windowSessionRelayoutInfo() ? new WindowRelayoutResult(mWinFrames, mMergedConfiguration, mSurfaceControl, mInsetsState, mTempControls) : null; private final Point mSurfaceSize = new Point(); private final Point mLastSurfaceSize = new Point(); private final Matrix mTmpMatrix = new Matrix(); private final float[] mTmpValues = new float[9]; final WindowManager.LayoutParams mLayout = new WindowManager.LayoutParams(); IWindowSession mSession; final Object mLock = new Object(); boolean mOffsetMessageEnqueued; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) @GuardedBy("mLock") private float mPendingXOffset; @GuardedBy("mLock") private float mPendingYOffset; @GuardedBy("mLock") private float mPendingXOffsetStep; @GuardedBy("mLock") private float mPendingYOffsetStep; /** * local color extraction related fields. When a user calls `addLocalColorAreas` */ @GuardedBy("mLock") private final ArraySet mLocalColorAreas = new ArraySet<>(4); @GuardedBy("mLock") private final ArraySet mLocalColorsToAdd = new ArraySet<>(4); private long mLastProcessLocalColorsTimestamp; private AtomicBoolean mProcessLocalColorsPending = new AtomicBoolean(false); private int mPixelCopyCount = 0; // 2D matrix [x][y] to represent a page of a portion of a window @GuardedBy("mLock") private EngineWindowPage[] mWindowPages = new EngineWindowPage[0]; private Bitmap mLastScreenshot; private boolean mResetWindowPages; boolean mPendingSync; MotionEvent mPendingMove; boolean mIsInAmbientMode; // used to throttle onComputeColors private long mLastColorInvalidation; private final Runnable mNotifyColorsChanged = this::notifyColorsChanged; private final Supplier mClockFunction; private final Handler mHandler; private Display mDisplay; private Context mDisplayContext; private int mDisplayState; private float mCustomDimAmount = 0f; private float mWallpaperDimAmount = 0f; private float mPreviousWallpaperDimAmount = mWallpaperDimAmount; private float mDefaultDimAmount = 0.05f; SurfaceControl mBbqSurfaceControl; BLASTBufferQueue mBlastBufferQueue; private SurfaceControl mScreenshotSurfaceControl; private Point mScreenshotSize = new Point(); final BaseSurfaceHolder mSurfaceHolder = new BaseSurfaceHolder() { { mRequestedFormat = PixelFormat.RGBX_8888; } @Override public boolean onAllowLockCanvas() { return mDrawingAllowed; } @Override public void onRelayoutContainer() { Message msg = mCaller.obtainMessage(MSG_UPDATE_SURFACE); mCaller.sendMessage(msg); } @Override public void onUpdateSurface() { Message msg = mCaller.obtainMessage(MSG_UPDATE_SURFACE); mCaller.sendMessage(msg); } public boolean isCreating() { return mIsCreating; } @Override public void setFixedSize(int width, int height) { if (!mFixedSizeAllowed && !mIWallpaperEngine.mIsPreview) { // Regular apps can't do this. It can only work for // certain designs of window animations, so you can't // rely on it. throw new UnsupportedOperationException( "Wallpapers currently only support sizing from layout"); } super.setFixedSize(width, height); } public void setKeepScreenOn(boolean screenOn) { throw new UnsupportedOperationException( "Wallpapers do not support keep screen on"); } private void prepareToDraw() { if (mDisplayState == Display.STATE_DOZE || mDisplayState == Display.STATE_DOZE_SUSPEND) { try { mSession.pokeDrawLock(mWindow); } catch (RemoteException e) { // System server died, can be ignored. } } } @Override public Canvas lockCanvas() { prepareToDraw(); return super.lockCanvas(); } @Override public Canvas lockCanvas(Rect dirty) { prepareToDraw(); return super.lockCanvas(dirty); } @Override public Canvas lockHardwareCanvas() { prepareToDraw(); return super.lockHardwareCanvas(); } }; final class WallpaperInputEventReceiver extends InputEventReceiver { public WallpaperInputEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); } @Override public void onInputEvent(InputEvent event) { boolean handled = false; try { if (event instanceof MotionEvent && (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { MotionEvent dup = MotionEvent.obtainNoHistory((MotionEvent)event); dispatchPointer(dup); handled = true; } } finally { finishInputEvent(event, handled); } } } WallpaperInputEventReceiver mInputEventReceiver; final BaseIWindow mWindow = new BaseIWindow() { @Override public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) { Message msg = mCaller.obtainMessageIO(MSG_WINDOW_RESIZED, reportDraw ? 1 : 0, mergedConfiguration); mIWallpaperEngine.mPendingResizeCount.incrementAndGet(); mCaller.sendMessage(msg); } @Override public void moved(int newX, int newY) { Message msg = mCaller.obtainMessageII(MSG_WINDOW_MOVED, newX, newY); mCaller.sendMessage(msg); } @Override public void dispatchAppVisibility(boolean visible) { // We don't do this in preview mode; we'll let the preview // activity tell us when to run. if (!mIWallpaperEngine.mIsPreview) { Message msg = mCaller.obtainMessageI(MSG_VISIBILITY_CHANGED, visible ? 1 : 0); mCaller.sendMessage(msg); } } @Override public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep, float zoom, boolean sync) { synchronized (mLock) { if (DEBUG) Log.v(TAG, "Dispatch wallpaper offsets: " + x + ", " + y); mPendingXOffset = x; mPendingYOffset = y; mPendingXOffsetStep = xStep; mPendingYOffsetStep = yStep; if (sync) { mPendingSync = true; } if (!mOffsetMessageEnqueued) { mOffsetMessageEnqueued = true; Message msg = mCaller.obtainMessage(MSG_WALLPAPER_OFFSETS); mCaller.sendMessage(msg); } Message msg = mCaller.obtainMessageI(MSG_ZOOM, Float.floatToIntBits(zoom)); mCaller.sendMessage(msg); } } @Override public void dispatchWallpaperCommand(String action, int x, int y, int z, Bundle extras, boolean sync) { synchronized (mLock) { if (DEBUG) Log.v(TAG, "Dispatch wallpaper command: " + x + ", " + y); WallpaperCommand cmd = new WallpaperCommand(); cmd.action = action; cmd.x = x; cmd.y = y; cmd.z = z; cmd.extras = extras; cmd.sync = sync; Message msg = mCaller.obtainMessage(MSG_WALLPAPER_COMMAND); msg.obj = cmd; mCaller.sendMessage(msg); } } }; /** * Default constructor */ public Engine() { this(SystemClock::elapsedRealtime, Handler.getMain()); } /** * Constructor used for test purposes. * * @param clockFunction Supplies current times in millis. * @param handler Used for posting/deferring asynchronous calls. * @hide */ @VisibleForTesting public Engine(Supplier clockFunction, Handler handler) { mClockFunction = clockFunction; mHandler = handler; } /** * Provides access to the surface in which this wallpaper is drawn. */ public SurfaceHolder getSurfaceHolder() { return mSurfaceHolder; } /** * Returns the current wallpaper flags indicating which screen this Engine is rendering to. */ @SetWallpaperFlags public int getWallpaperFlags() { return mIWallpaperEngine.mWhich; } /** * Convenience for {@link WallpaperManager#getDesiredMinimumWidth() * WallpaperManager.getDesiredMinimumWidth()}, returning the width * that the system would like this wallpaper to run in. */ public int getDesiredMinimumWidth() { return mIWallpaperEngine.mReqWidth; } /** * Convenience for {@link WallpaperManager#getDesiredMinimumHeight() * WallpaperManager.getDesiredMinimumHeight()}, returning the height * that the system would like this wallpaper to run in. */ public int getDesiredMinimumHeight() { return mIWallpaperEngine.mReqHeight; } /** * Return whether the wallpaper is currently visible to the user, * this is the last value supplied to * {@link #onVisibilityChanged(boolean)}. */ public boolean isVisible() { return mReportedVisible; } /** * Return whether the wallpaper is capable of extracting local colors in a rectangle area, * Must implement without calling super: * {@link #addLocalColorsAreas(List)} * {@link #removeLocalColorsAreas(List)} * When local colors change, call {@link #notifyLocalColorsChanged(List, List)} * See {@link com.android.systemui.wallpapers.ImageWallpaper} for an example * @hide */ public boolean supportsLocalColorExtraction() { return false; } /** * Returns true if this engine is running in preview mode -- that is, * it is being shown to the user before they select it as the actual * wallpaper. */ public boolean isPreview() { return mIWallpaperEngine.mIsPreview; } /** * Returns true if this engine is running in ambient mode -- that is, * it is being shown in low power mode, on always on display. * @hide */ @SystemApi public boolean isInAmbientMode() { return mIsInAmbientMode; } /** * This will be called when the wallpaper is first started. If true is returned, the system * will zoom in the wallpaper by default and zoom it out as the user interacts, * to create depth. Otherwise, zoom will have to be handled manually * in {@link #onZoomChanged(float)}. * * @hide */ public boolean shouldZoomOutWallpaper() { return mIsWearOs && !CompatChanges.isChangeEnabled(WEAROS_WALLPAPER_HANDLES_SCALING); } /** * This will be called in the end of {@link #updateSurface(boolean, boolean, boolean)}. * If true is returned, the engine will not report shown until rendering finished is * reported. Otherwise, the engine will report shown immediately right after redraw phase * in {@link #updateSurface(boolean, boolean, boolean)}. * * @hide */ public boolean shouldWaitForEngineShown() { return false; } /** * Reports the rendering is finished, stops waiting, then invokes * {@link IWallpaperEngineWrapper#reportShown()}. * * @hide */ public void reportEngineShown(boolean waitForEngineShown) { if (mIWallpaperEngine.mShownReported) return; Trace.beginSection("WPMS.reportEngineShown-" + waitForEngineShown); Log.d(TAG, "reportEngineShown: shouldWait=" + waitForEngineShown); if (!waitForEngineShown) { Message message = mCaller.obtainMessage(MSG_REPORT_SHOWN); mCaller.removeMessages(MSG_REPORT_SHOWN); mCaller.sendMessage(message); } else { // if we are already waiting, no need to reset the timeout. if (!mCaller.hasMessages(MSG_REPORT_SHOWN)) { Message message = mCaller.obtainMessage(MSG_REPORT_SHOWN); mCaller.sendMessageDelayed(message, TimeUnit.SECONDS.toMillis(5)); } } Trace.endSection(); } /** * Control whether this wallpaper will receive raw touch events * from the window manager as the user interacts with the window * that is currently displaying the wallpaper. By default they * are turned off. If enabled, the events will be received in * {@link #onTouchEvent(MotionEvent)}. */ public void setTouchEventsEnabled(boolean enabled) { mWindowFlags = enabled ? (mWindowFlags&~WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) : (mWindowFlags|WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); if (mCreated) { updateSurface(false, false, false); } } /** * Control whether this wallpaper will receive notifications when the wallpaper * has been scrolled. By default, wallpapers will receive notifications, although * the default static image wallpapers do not. It is a performance optimization to * set this to false. * * @param enabled whether the wallpaper wants to receive offset notifications */ public void setOffsetNotificationsEnabled(boolean enabled) { mWindowPrivateFlags = enabled ? (mWindowPrivateFlags | WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS) : (mWindowPrivateFlags & ~WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS); if (mCreated) { updateSurface(false, false, false); } } /** @hide */ public void setShowForAllUsers(boolean show) { mWindowPrivateFlags = show ? (mWindowPrivateFlags | WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS) : (mWindowPrivateFlags & ~WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); if (mCreated) { updateSurface(false, false, false); } } /** {@hide} */ @UnsupportedAppUsage public void setFixedSizeAllowed(boolean allowed) { mFixedSizeAllowed = allowed; } /** * Returns the current scale of the surface * @hide */ @VisibleForTesting public float getZoom() { return mZoom; } /** * Called once to initialize the engine. After returning, the * engine's surface will be created by the framework. */ @MainThread public void onCreate(SurfaceHolder surfaceHolder) { } /** * Called right before the engine is going away. After this the * surface will be destroyed and this Engine object is no longer * valid. */ @MainThread public void onDestroy() { } /** * Called to inform you of the wallpaper becoming visible or * hidden. It is very important that a wallpaper only use * CPU while it is visible.. */ @MainThread public void onVisibilityChanged(boolean visible) { } /** * Called with the current insets that are in effect for the wallpaper. * This gives you the part of the overall wallpaper surface that will * generally be visible to the user (ignoring position offsets applied to it). * * @param insets Insets to apply. */ @MainThread public void onApplyWindowInsets(WindowInsets insets) { } /** * Called as the user performs touch-screen interaction with the * window that is currently showing this wallpaper. Note that the * events you receive here are driven by the actual application the * user is interacting with, so if it is slow you will get fewer * move events. */ @MainThread public void onTouchEvent(MotionEvent event) { } /** * Called to inform you of the wallpaper's offsets changing * within its contain, corresponding to the container's * call to {@link WallpaperManager#setWallpaperOffsets(IBinder, float, float) * WallpaperManager.setWallpaperOffsets()}. */ @MainThread public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) { } /** * Process a command that was sent to the wallpaper with * {@link WallpaperManager#sendWallpaperCommand}. * The default implementation does nothing, and always returns null * as the result. * * @param action The name of the command to perform. This tells you * what to do and how to interpret the rest of the arguments. * @param x Generic integer parameter. * @param y Generic integer parameter. * @param z Generic integer parameter. * @param extras Any additional parameters. * @param resultRequested If true, the caller is requesting that * a result, appropriate for the command, be returned back. * @return If returning a result, create a Bundle and place the * result data in to it. Otherwise return null. */ @MainThread public Bundle onCommand(String action, int x, int y, int z, Bundle extras, boolean resultRequested) { return null; } /** * Called when the device enters or exits ambient mode. * * @param inAmbientMode {@code true} if in ambient mode. * @param animationDuration How long the transition animation to change the ambient state * should run, in milliseconds. If 0 is passed as the argument * here, the state should be switched immediately. * * @see #isInAmbientMode() * @see WallpaperInfo#supportsAmbientMode() * @hide */ @SystemApi @MainThread public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) { } /** * Called when the dim amount of the wallpaper changed. This can be used to recompute the * wallpaper colors based on the new dim, and call {@link #notifyColorsChanged()}. * @hide */ @FlaggedApi(FLAG_OFFLOAD_COLOR_EXTRACTION) public void onDimAmountChanged(float dimAmount) { } /** * Called when an application has changed the desired virtual size of * the wallpaper. */ @MainThread public void onDesiredSizeChanged(int desiredWidth, int desiredHeight) { } /** * Convenience for {@link SurfaceHolder.Callback#surfaceChanged * SurfaceHolder.Callback.surfaceChanged()}. */ @MainThread public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { } /** * Convenience for {@link SurfaceHolder.Callback2#surfaceRedrawNeeded * SurfaceHolder.Callback.surfaceRedrawNeeded()}. */ @MainThread public void onSurfaceRedrawNeeded(SurfaceHolder holder) { } /** * Convenience for {@link SurfaceHolder.Callback#surfaceCreated * SurfaceHolder.Callback.surfaceCreated()}. */ @MainThread public void onSurfaceCreated(SurfaceHolder holder) { } /** * Convenience for {@link SurfaceHolder.Callback#surfaceDestroyed * SurfaceHolder.Callback.surfaceDestroyed()}. */ @MainThread public void onSurfaceDestroyed(SurfaceHolder holder) { } /** * Called when the current wallpaper flags change. * * @param which The new flag value * @see #getWallpaperFlags() */ @MainThread public void onWallpaperFlagsChanged(@SetWallpaperFlags int which) { } /** * Called when the zoom level of the wallpaper changed. * This method will be called with the initial zoom level when the surface is created. * * @param zoom the zoom level, between 0 indicating fully zoomed in and 1 indicating fully * zoomed out. */ @MainThread public void onZoomChanged(@FloatRange(from = 0f, to = 1f) float zoom) { } /** * Notifies the engine that wallpaper colors changed significantly. * This will trigger a {@link #onComputeColors()} call. */ public void notifyColorsChanged() { if (mDestroyed) { Log.i(TAG, "Ignoring notifyColorsChanged(), Engine has already been destroyed."); return; } final long now = mClockFunction.get(); if (now - mLastColorInvalidation < NOTIFY_COLORS_RATE_LIMIT_MS) { Log.w(TAG, "This call has been deferred. You should only call " + "notifyColorsChanged() once every " + (NOTIFY_COLORS_RATE_LIMIT_MS / 1000f) + " seconds."); if (!mHandler.hasCallbacks(mNotifyColorsChanged)) { mHandler.postDelayed(mNotifyColorsChanged, NOTIFY_COLORS_RATE_LIMIT_MS); } return; } mLastColorInvalidation = now; mHandler.removeCallbacks(mNotifyColorsChanged); try { final WallpaperColors newColors = onComputeColors(); if (mConnection != null) { mConnection.onWallpaperColorsChanged(newColors, mDisplay.getDisplayId()); } else { Log.w(TAG, "Can't notify system because wallpaper connection " + "was not established."); } mResetWindowPages = true; processLocalColors(); } catch (RemoteException e) { Log.w(TAG, "Can't notify system because wallpaper connection was lost.", e); } } /** * Called by the system when it needs to know what colors the wallpaper is using. * You might return null if no color information is available at the moment. * In that case you might want to call {@link #notifyColorsChanged()} when * color information becomes available. *

* The simplest way of creating a {@link android.app.WallpaperColors} object is by using * {@link android.app.WallpaperColors#fromBitmap(Bitmap)} or * {@link android.app.WallpaperColors#fromDrawable(Drawable)}, but you can also specify * your main colors by constructing a {@link android.app.WallpaperColors} object manually. * * @return Wallpaper colors. */ @MainThread public @Nullable WallpaperColors onComputeColors() { return null; } /** * Send the changed local color areas for the connection * @param regions * @param colors * @hide */ public void notifyLocalColorsChanged(@NonNull List regions, @NonNull List colors) throws RuntimeException { for (int i = 0; i < regions.size() && i < colors.size(); i++) { WallpaperColors color = colors.get(i); RectF area = regions.get(i); if (color == null || area == null) { if (DEBUG) { Log.e(TAG, "notifyLocalColorsChanged null values. color: " + color + " area " + area); } continue; } try { mConnection.onLocalWallpaperColorsChanged( area, color, mDisplayContext.getDisplayId() ); } catch (RemoteException e) { throw new RuntimeException(e); } } WallpaperColors primaryColors = mIWallpaperEngine.mWallpaperManager .getWallpaperColors(WallpaperManager.FLAG_SYSTEM); setPrimaryWallpaperColors(primaryColors); } private void setPrimaryWallpaperColors(WallpaperColors colors) { if (colors == null) { return; } int colorHints = colors.getColorHints(); mShouldDimByDefault = ((colorHints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0 && (colorHints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) == 0); // Recompute dim in case it changed compared to the previous WallpaperService updateWallpaperDimming(mCustomDimAmount); } /** * Update the dim amount of the wallpaper by updating the surface. * * @param dimAmount Float amount between [0.0, 1.0] to dim the wallpaper. */ private void updateWallpaperDimming(float dimAmount) { mCustomDimAmount = Math.min(1f, dimAmount); // If default dim is enabled, the actual dim amount is at least the default dim amount mWallpaperDimAmount = (!mShouldDimByDefault) ? mCustomDimAmount : Math.max(mDefaultDimAmount, mCustomDimAmount); if (!ENABLE_WALLPAPER_DIMMING || mBbqSurfaceControl == null || !mBbqSurfaceControl.isValid() || mWallpaperDimAmount == mPreviousWallpaperDimAmount) { return; } SurfaceControl.Transaction surfaceControlTransaction = new SurfaceControl.Transaction(); // TODO: apply the dimming to preview as well once surface transparency works in // preview mode. if (!isPreview()) { Log.v(TAG, "Setting wallpaper dimming: " + mWallpaperDimAmount); // Animate dimming to gradually change the wallpaper alpha from the previous // dim amount to the new amount only if the dim amount changed. ValueAnimator animator = ValueAnimator.ofFloat( mPreviousWallpaperDimAmount, mWallpaperDimAmount); animator.setDuration(DIMMING_ANIMATION_DURATION_MS); animator.addUpdateListener((ValueAnimator va) -> { final float dimValue = (float) va.getAnimatedValue(); if (mBbqSurfaceControl != null) { surfaceControlTransaction .setAlpha(mBbqSurfaceControl, 1 - dimValue).apply(); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { updateSurface(false, false, true); } }); animator.start(); } else { Log.v(TAG, "Setting wallpaper dimming: " + 0); surfaceControlTransaction.setAlpha(mBbqSurfaceControl, 1.0f).apply(); updateSurface(false, false, true); } mPreviousWallpaperDimAmount = mWallpaperDimAmount; // after the dim changes, allow colors to be immediately recomputed mLastColorInvalidation = 0; if (offloadColorExtraction()) onDimAmountChanged(mWallpaperDimAmount); } /** * Sets internal engine state. Only for testing. * @param created {@code true} or {@code false}. * @hide */ @VisibleForTesting public void setCreated(boolean created) { mCreated = created; } protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) { out.print(prefix); out.print("mInitializing="); out.print(mInitializing); out.print(" mDestroyed="); out.println(mDestroyed); out.print(prefix); out.print("mVisible="); out.print(mVisible); out.print(" mReportedVisible="); out.println(mReportedVisible); out.print(" mIsScreenTurningOn="); out.println(mIsScreenTurningOn); out.print(prefix); out.print("mDisplay="); out.println(mDisplay); out.print(prefix); out.print("mCreated="); out.print(mCreated); out.print(" mSurfaceCreated="); out.print(mSurfaceCreated); out.print(" mIsCreating="); out.print(mIsCreating); out.print(" mDrawingAllowed="); out.println(mDrawingAllowed); out.print(prefix); out.print("mWidth="); out.print(mWidth); out.print(" mCurWidth="); out.print(mCurWidth); out.print(" mHeight="); out.print(mHeight); out.print(" mCurHeight="); out.println(mCurHeight); out.print(prefix); out.print("mType="); out.print(mType); out.print(" mWindowFlags="); out.print(mWindowFlags); out.print(" mCurWindowFlags="); out.println(mCurWindowFlags); out.print(prefix); out.print("mWindowPrivateFlags="); out.print(mWindowPrivateFlags); out.print(" mCurWindowPrivateFlags="); out.println(mCurWindowPrivateFlags); out.print(prefix); out.println("mWinFrames="); out.println(mWinFrames); out.print(prefix); out.print("mConfiguration="); out.println(mMergedConfiguration.getMergedConfiguration()); out.print(prefix); out.print("mLayout="); out.println(mLayout); out.print(prefix); out.print("mZoom="); out.println(mZoom); out.print(prefix); out.print("mPreviewSurfacePosition="); out.println(mPreviewSurfacePosition); final int pendingCount = mIWallpaperEngine.mPendingResizeCount.get(); if (pendingCount != 0) { out.print(prefix); out.print("mPendingResizeCount="); out.println(pendingCount); } if (mPreserveVisible) { out.print(prefix); out.print("mPreserveVisible=true"); } synchronized (mLock) { out.print(prefix); out.print("mPendingXOffset="); out.print(mPendingXOffset); out.print(" mPendingXOffset="); out.println(mPendingXOffset); out.print(prefix); out.print("mPendingXOffsetStep="); out.print(mPendingXOffsetStep); out.print(" mPendingXOffsetStep="); out.println(mPendingXOffsetStep); out.print(prefix); out.print("mOffsetMessageEnqueued="); out.print(mOffsetMessageEnqueued); out.print(" mPendingSync="); out.println(mPendingSync); if (mPendingMove != null) { out.print(prefix); out.print("mPendingMove="); out.println(mPendingMove); } } } /** * Set the wallpaper zoom to the given value. This value will be ignored when in ambient * mode (and zoom will be reset to 0). * @hide * @param zoom between 0 and 1 (inclusive) indicating fully zoomed in to fully zoomed out * respectively. */ @VisibleForTesting public void setZoom(float zoom) { if (DEBUG) { Log.v(TAG, "set zoom received: " + zoom); } boolean updated = false; synchronized (mLock) { if (DEBUG) { Log.v(TAG, "mZoom: " + mZoom + " updated: " + zoom); } if (mIsInAmbientMode) { mZoom = 0; } if (Float.compare(zoom, mZoom) != 0) { mZoom = zoom; updated = true; } } if (DEBUG) Log.v(TAG, "setZoom updated? " + updated); if (updated && !mDestroyed) { onZoomChanged(mZoom); } } private void dispatchPointer(MotionEvent event) { if (event.isTouchEvent()) { synchronized (mLock) { if (event.getAction() == MotionEvent.ACTION_MOVE) { mPendingMove = event; } else { mPendingMove = null; } } Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event); mCaller.sendMessage(msg); } else { event.recycle(); } } void updateSurface(boolean forceRelayout, boolean forceReport, boolean redrawNeeded) { if (mDestroyed) { Log.w(TAG, "Ignoring updateSurface due to destroyed"); return; } boolean fixedSize = false; int myWidth = mSurfaceHolder.getRequestedWidth(); if (myWidth <= 0) myWidth = ViewGroup.LayoutParams.MATCH_PARENT; else fixedSize = true; int myHeight = mSurfaceHolder.getRequestedHeight(); if (myHeight <= 0) myHeight = ViewGroup.LayoutParams.MATCH_PARENT; else fixedSize = true; final boolean creating = !mCreated; final boolean surfaceCreating = !mSurfaceCreated; final boolean formatChanged = mFormat != mSurfaceHolder.getRequestedFormat(); boolean sizeChanged = mWidth != myWidth || mHeight != myHeight; boolean insetsChanged = !mCreated; final boolean typeChanged = mType != mSurfaceHolder.getRequestedType(); final boolean flagsChanged = mCurWindowFlags != mWindowFlags || mCurWindowPrivateFlags != mWindowPrivateFlags; if (forceRelayout || creating || surfaceCreating || formatChanged || sizeChanged || typeChanged || flagsChanged || redrawNeeded || !mIWallpaperEngine.mShownReported) { if (DEBUG) Log.v(TAG, "Changes: creating=" + creating + " format=" + formatChanged + " size=" + sizeChanged); try { mWidth = myWidth; mHeight = myHeight; mFormat = mSurfaceHolder.getRequestedFormat(); mType = mSurfaceHolder.getRequestedType(); mLayout.x = 0; mLayout.y = 0; mLayout.format = mFormat; mCurWindowFlags = mWindowFlags; mLayout.flags = mWindowFlags | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; final Configuration config = mMergedConfiguration.getMergedConfiguration(); final Rect maxBounds = new Rect(config.windowConfiguration.getMaxBounds()); if (myWidth == ViewGroup.LayoutParams.MATCH_PARENT && myHeight == ViewGroup.LayoutParams.MATCH_PARENT) { mLayout.width = myWidth; mLayout.height = myHeight; mLayout.flags &= ~WindowManager.LayoutParams.FLAG_SCALED; } else { final float layoutScale = Math.max( maxBounds.width() / (float) myWidth, maxBounds.height() / (float) myHeight); mLayout.width = (int) (myWidth * layoutScale + .5f); mLayout.height = (int) (myHeight * layoutScale + .5f); mLayout.flags |= WindowManager.LayoutParams.FLAG_SCALED; } mCurWindowPrivateFlags = mWindowPrivateFlags; mLayout.privateFlags = mWindowPrivateFlags; mLayout.memoryType = mType; mLayout.token = mWindowToken; if (!mCreated) { // Add window mLayout.type = mIWallpaperEngine.mWindowType; mLayout.gravity = Gravity.START|Gravity.TOP; mLayout.setFitInsetsTypes(0 /* types */); mLayout.setTitle(WallpaperService.this.getClass().getName()); mLayout.windowAnimations = com.android.internal.R.style.Animation_Wallpaper; InputChannel inputChannel = new InputChannel(); if (mSession.addToDisplay(mWindow, mLayout, View.VISIBLE, mDisplay.getDisplayId(), WindowInsets.Type.defaultVisible(), inputChannel, mInsetsState, mTempControls, new Rect(), new float[1]) < 0) { Log.w(TAG, "Failed to add window while updating wallpaper surface."); return; } mSession.setShouldZoomOutWallpaper(mWindow, shouldZoomOutWallpaper()); mCreated = true; mInputEventReceiver = new WallpaperInputEventReceiver( inputChannel, Looper.myLooper()); } mSurfaceHolder.mSurfaceLock.lock(); mDrawingAllowed = true; if (!fixedSize) { mLayout.surfaceInsets.set(mIWallpaperEngine.mDisplayPadding); } else { mLayout.surfaceInsets.set(0, 0, 0, 0); } final int relayoutResult; if (windowSessionRelayoutInfo()) { relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight, View.VISIBLE, 0, 0, 0, mRelayoutResult); } else { relayoutResult = mSession.relayoutLegacy(mWindow, mLayout, mWidth, mHeight, View.VISIBLE, 0, 0, 0, mWinFrames, mMergedConfiguration, mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle); } final Rect outMaxBounds = mMergedConfiguration.getMergedConfiguration() .windowConfiguration.getMaxBounds(); if (!outMaxBounds.equals(maxBounds)) { Log.i(TAG, "Retry updateSurface because bounds changed from relayout: " + maxBounds + " -> " + outMaxBounds); mSurfaceHolder.mSurfaceLock.unlock(); mDrawingAllowed = false; mCaller.sendMessage(mCaller.obtainMessageI(MSG_WINDOW_RESIZED, redrawNeeded ? 1 : 0)); return; } final int transformHint = SurfaceControl.rotationToBufferTransform( (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4); mSurfaceControl.setTransformHint(transformHint); WindowLayout.computeSurfaceSize(mLayout, maxBounds, mWidth, mHeight, mWinFrames.frame, false /* dragResizing */, mSurfaceSize); if (mSurfaceControl.isValid()) { if (mBbqSurfaceControl == null) { mBbqSurfaceControl = new SurfaceControl.Builder() .setName("Wallpaper BBQ wrapper") .setHidden(false) .setBLASTLayer() .setParent(mSurfaceControl) .setCallsite("Wallpaper#relayout") .build(); SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); final int frameRateCompat = getResources().getInteger( R.integer.config_wallpaperFrameRateCompatibility); if (DEBUG) { Log.d(TAG, "Set frame rate compatibility value for Wallpaper: " + frameRateCompat); } transaction.setDefaultFrameRateCompatibility(mBbqSurfaceControl, frameRateCompat).apply(); } // Propagate transform hint from WM, so we can use the right hint for the // first frame. mBbqSurfaceControl.setTransformHint(transformHint); Surface blastSurface = getOrCreateBLASTSurface(mSurfaceSize.x, mSurfaceSize.y, mFormat); // If blastSurface == null that means it hasn't changed since the last // time we called. In this situation, avoid calling transferFrom as we // would then inc the generation ID and cause EGL resources to be recreated. if (blastSurface != null) { mSurfaceHolder.mSurface.transferFrom(blastSurface); } } if (!mLastSurfaceSize.equals(mSurfaceSize)) { mLastSurfaceSize.set(mSurfaceSize.x, mSurfaceSize.y); } if (DEBUG) Log.v(TAG, "New surface: " + mSurfaceHolder.mSurface + ", frame=" + mWinFrames); int w = mWinFrames.frame.width(); int h = mWinFrames.frame.height(); final DisplayCutout rawCutout = mInsetsState.getDisplayCutout(); final Rect visibleFrame = new Rect(mWinFrames.frame); visibleFrame.intersect(mInsetsState.getDisplayFrame()); WindowInsets windowInsets = mInsetsState.calculateInsets(visibleFrame, null /* ignoringVisibilityState */, config.isScreenRound(), mLayout.softInputMode, mLayout.flags, SYSTEM_UI_FLAG_VISIBLE, mLayout.type, config.windowConfiguration.getActivityType(), null /* idSideMap */); if (!fixedSize) { final Rect padding = mIWallpaperEngine.mDisplayPadding; w += padding.left + padding.right; h += padding.top + padding.bottom; windowInsets = windowInsets.insetUnchecked( -padding.left, -padding.top, -padding.right, -padding.bottom); } else { w = myWidth; h = myHeight; } if (mCurWidth != w) { sizeChanged = true; mCurWidth = w; } if (mCurHeight != h) { sizeChanged = true; mCurHeight = h; } if (DEBUG) { Log.v(TAG, "Wallpaper size has changed: (" + mCurWidth + ", " + mCurHeight); } final Rect contentInsets = windowInsets.getSystemWindowInsets().toRect(); final Rect stableInsets = windowInsets.getStableInsets().toRect(); final DisplayCutout displayCutout = windowInsets.getDisplayCutout() != null ? windowInsets.getDisplayCutout() : rawCutout; insetsChanged |= !mDispatchedContentInsets.equals(contentInsets); insetsChanged |= !mDispatchedStableInsets.equals(stableInsets); insetsChanged |= !mDispatchedDisplayCutout.equals(displayCutout); mSurfaceHolder.setSurfaceFrameSize(w, h); mSurfaceHolder.mSurfaceLock.unlock(); if (!mSurfaceHolder.mSurface.isValid()) { reportSurfaceDestroyed(); if (DEBUG) Log.v(TAG, "Layout: Surface destroyed"); return; } boolean didSurface = false; try { mSurfaceHolder.ungetCallbacks(); if (surfaceCreating) { mIsCreating = true; didSurface = true; if (DEBUG) Log.v(TAG, "onSurfaceCreated(" + mSurfaceHolder + "): " + this); Trace.beginSection("WPMS.Engine.onSurfaceCreated"); onSurfaceCreated(mSurfaceHolder); Trace.endSection(); SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); if (callbacks != null) { for (SurfaceHolder.Callback c : callbacks) { c.surfaceCreated(mSurfaceHolder); } } } redrawNeeded |= creating || (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0; if (forceReport || creating || surfaceCreating || formatChanged || sizeChanged) { if (DEBUG) { RuntimeException e = new RuntimeException(); e.fillInStackTrace(); Log.w(TAG, "forceReport=" + forceReport + " creating=" + creating + " formatChanged=" + formatChanged + " sizeChanged=" + sizeChanged, e); } if (DEBUG) Log.v(TAG, "onSurfaceChanged(" + mSurfaceHolder + ", " + mFormat + ", " + mCurWidth + ", " + mCurHeight + "): " + this); didSurface = true; Trace.beginSection("WPMS.Engine.onSurfaceChanged"); onSurfaceChanged(mSurfaceHolder, mFormat, mCurWidth, mCurHeight); Trace.endSection(); SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); if (callbacks != null) { for (SurfaceHolder.Callback c : callbacks) { c.surfaceChanged(mSurfaceHolder, mFormat, mCurWidth, mCurHeight); } } } if (insetsChanged) { mDispatchedContentInsets.set(contentInsets); mDispatchedStableInsets.set(stableInsets); mDispatchedDisplayCutout = displayCutout; if (DEBUG) { Log.v(TAG, "dispatching insets=" + windowInsets); } Trace.beginSection("WPMS.Engine.onApplyWindowInsets"); onApplyWindowInsets(windowInsets); Trace.endSection(); } if (redrawNeeded || sizeChanged) { Trace.beginSection("WPMS.Engine.onSurfaceRedrawNeeded"); onSurfaceRedrawNeeded(mSurfaceHolder); Trace.endSection(); SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); if (callbacks != null) { for (SurfaceHolder.Callback c : callbacks) { if (c instanceof SurfaceHolder.Callback2) { ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( mSurfaceHolder); } } } } if (didSurface && !mReportedVisible) { if (mIsCreating) { // The surface has been created, but the wallpaper isn't visible. // Trigger onVisibilityChanged(true) then onVisibilityChanged(false) // to make sure the wallpaper is stopped even after the events // onSurfaceCreated() and onSurfaceChanged(). if (noConsecutiveVisibilityEvents()) { if (DEBUG) Log.v(TAG, "toggling onVisibilityChanged"); Trace.beginSection("WPMS.Engine.onVisibilityChanged-true"); onVisibilityChanged(true); Trace.endSection(); Trace.beginSection("WPMS.Engine.onVisibilityChanged-false"); onVisibilityChanged(false); Trace.endSection(); } else { if (DEBUG) { Log.v(TAG, "onVisibilityChanged(true) at surface: " + this); } Trace.beginSection("WPMS.Engine.onVisibilityChanged-true"); onVisibilityChanged(true); Trace.endSection(); } } if (!noConsecutiveVisibilityEvents()) { if (DEBUG) { Log.v(TAG, "onVisibilityChanged(false) at surface: " + this); } Trace.beginSection("WPMS.Engine.onVisibilityChanged-false"); onVisibilityChanged(false); Trace.endSection(); } } } finally { mIsCreating = false; mSurfaceCreated = true; if (redrawNeeded) { mSession.finishDrawing(mWindow, null /* postDrawTransaction */, Integer.MAX_VALUE); processLocalColors(); } reposition(); reportEngineShown(shouldWaitForEngineShown()); } } catch (RemoteException ex) { } if (DEBUG) Log.v( TAG, "Layout: x=" + mLayout.x + " y=" + mLayout.y + " w=" + mLayout.width + " h=" + mLayout.height); } } private void resizePreview(Rect position) { if (position != null) { mSurfaceHolder.setFixedSize(position.width(), position.height()); } } private void reposition() { if (mPreviewSurfacePosition == null) { return; } if (DEBUG) { Log.i(TAG, "reposition: rect: " + mPreviewSurfacePosition); } mTmpMatrix.setTranslate(mPreviewSurfacePosition.left, mPreviewSurfacePosition.top); mTmpMatrix.postScale(((float) mPreviewSurfacePosition.width()) / mCurWidth, ((float) mPreviewSurfacePosition.height()) / mCurHeight); mTmpMatrix.getValues(mTmpValues); SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.setPosition(mSurfaceControl, mPreviewSurfacePosition.left, mPreviewSurfacePosition.top); t.setMatrix(mSurfaceControl, mTmpValues[MSCALE_X], mTmpValues[MSKEW_Y], mTmpValues[MSKEW_X], mTmpValues[MSCALE_Y]); t.apply(); } void attach(IWallpaperEngineWrapper wrapper) { if (DEBUG) Log.v(TAG, "attach: " + this + " wrapper=" + wrapper); if (mDestroyed) { return; } mIWallpaperEngine = wrapper; mCaller = wrapper.mCaller; mConnection = wrapper.mConnection; mWindowToken = wrapper.mWindowToken; mSurfaceHolder.setSizeFromLayout(); mInitializing = true; mSession = WindowManagerGlobal.getWindowSession(); mWindow.setSession(mSession); mLayout.packageName = getPackageName(); mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener, mCaller.getHandler()); mDisplay = mIWallpaperEngine.mDisplay; // Use window context of TYPE_WALLPAPER so client can access UI resources correctly. mDisplayContext = createDisplayContext(mDisplay) .createWindowContext(TYPE_WALLPAPER, null /* options */); mDefaultDimAmount = mDisplayContext.getResources().getFloat( com.android.internal.R.dimen.config_wallpaperDimAmount); mDisplayState = mDisplay.getCommittedState(); mMergedConfiguration.setOverrideConfiguration( mDisplayContext.getResources().getConfiguration()); if (DEBUG) Log.v(TAG, "onCreate(): " + this); Trace.beginSection("WPMS.Engine.onCreate"); onCreate(mSurfaceHolder); Trace.endSection(); mInitializing = false; mReportedVisible = false; Trace.beginSection("WPMS.Engine.updateSurface"); updateSurface(false, false, false); Trace.endSection(); } /** * The {@link Context} with resources that match the current display the wallpaper is on. * For multiple display environment, multiple engines can be created to render on each * display, but these displays may have different densities. Use this context to get the * corresponding resources for currently display, avoiding the context of the service. *

* The display context will never be {@code null} after * {@link Engine#onCreate(SurfaceHolder)} has been called. * * @return A {@link Context} for current display. */ @Nullable public Context getDisplayContext() { return mDisplayContext; } /** * Executes life cycle event and updates internal ambient mode state based on * message sent from handler. * * @param inAmbientMode {@code true} if in ambient mode. * @param animationDuration For how long the transition will last, in ms. * @hide */ @VisibleForTesting public void doAmbientModeChanged(boolean inAmbientMode, long animationDuration) { if (!mDestroyed) { if (DEBUG) { Log.v(TAG, "onAmbientModeChanged(" + inAmbientMode + ", " + animationDuration + "): " + this); } mIsInAmbientMode = inAmbientMode; if (mCreated) { onAmbientModeChanged(inAmbientMode, animationDuration); } } } void doDesiredSizeChanged(int desiredWidth, int desiredHeight) { if (!mDestroyed) { if (DEBUG) Log.v(TAG, "onDesiredSizeChanged(" + desiredWidth + "," + desiredHeight + "): " + this); mIWallpaperEngine.mReqWidth = desiredWidth; mIWallpaperEngine.mReqHeight = desiredHeight; onDesiredSizeChanged(desiredWidth, desiredHeight); doOffsetsChanged(true); } } void doDisplayPaddingChanged(Rect padding) { if (!mDestroyed) { if (DEBUG) Log.v(TAG, "onDisplayPaddingChanged(" + padding + "): " + this); if (!mIWallpaperEngine.mDisplayPadding.equals(padding)) { mIWallpaperEngine.mDisplayPadding.set(padding); updateSurface(true, false, false); } } } void onScreenTurningOnChanged(boolean isScreenTurningOn) { if (!mDestroyed) { mIsScreenTurningOn = isScreenTurningOn; reportVisibility(false); } } void doVisibilityChanged(boolean visible) { if (!mDestroyed) { mVisible = visible; reportVisibility(false); if (mReportedVisible) processLocalColors(); } else { AnimationHandler.requestAnimatorsEnabled(visible, this); } } void reportVisibility(boolean forceReport) { if (mScreenshotSurfaceControl != null && mVisible) { if (DEBUG) Log.v(TAG, "Frozen so don't report visibility change"); return; } if (!mDestroyed) { mDisplayState = mDisplay == null ? Display.STATE_UNKNOWN : mDisplay.getCommittedState(); boolean displayFullyOn = Display.isOnState(mDisplayState) && !mIsScreenTurningOn; boolean supportsAmbientMode = mIWallpaperEngine.mInfo == null ? false : mIWallpaperEngine.mInfo.supportsAmbientMode(); // Report visibility only if display is fully on or wallpaper supports ambient mode. final boolean visible = (mVisible && (displayFullyOn || supportsAmbientMode)) || mPreserveVisible; if (DEBUG) { Log.v( TAG, "reportVisibility" + " mReportedVisible=" + mReportedVisible + " mVisible=" + mVisible + " mDisplayState=" + mDisplayState); } if (mReportedVisible != visible || forceReport) { mReportedVisible = visible; if (DEBUG) { Log.v( TAG, "onVisibilityChanged(" + visible + "): " + this + " forceReport=" + forceReport); } if (visible) { // If becoming visible, in preview mode the surface // may have been destroyed so now we need to make // sure it is re-created. doOffsetsChanged(false); // It will check mSurfaceCreated so no need to force relayout. updateSurface(false /* forceRelayout */, false /* forceReport */, false /* redrawNeeded */); } onVisibilityChanged(visible); if (mReportedVisible && mFrozenRequested) { if (DEBUG) Log.v(TAG, "Freezing wallpaper after visibility update"); freeze(); } AnimationHandler.requestAnimatorsEnabled(visible, this); } } } void doOffsetsChanged(boolean always) { if (mDestroyed) { return; } if (!always && !mOffsetsChanged) { return; } float xOffset; float yOffset; float xOffsetStep; float yOffsetStep; boolean sync; synchronized (mLock) { xOffset = mPendingXOffset; yOffset = mPendingYOffset; xOffsetStep = mPendingXOffsetStep; yOffsetStep = mPendingYOffsetStep; sync = mPendingSync; mPendingSync = false; mOffsetMessageEnqueued = false; } if (mSurfaceCreated) { if (mReportedVisible) { if (DEBUG) Log.v(TAG, "Offsets change in " + this + ": " + xOffset + "," + yOffset); final int availw = mIWallpaperEngine.mReqWidth-mCurWidth; final int xPixels = availw > 0 ? -(int)(availw*xOffset+.5f) : 0; final int availh = mIWallpaperEngine.mReqHeight-mCurHeight; final int yPixels = availh > 0 ? -(int)(availh*yOffset+.5f) : 0; onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixels, yPixels); } else { mOffsetsChanged = true; } } if (sync) { try { if (DEBUG) Log.v(TAG, "Reporting offsets change complete"); mSession.wallpaperOffsetsComplete(mWindow.asBinder()); } catch (RemoteException e) { } } // setup local color extraction data processLocalColors(); } /** * Thread-safe util to call {@link #processLocalColorsInternal} with a minimum interval of * {@link #PROCESS_LOCAL_COLORS_INTERVAL_MS} between two calls. */ private void processLocalColors() { if (mProcessLocalColorsPending.compareAndSet(false, true)) { final long now = mClockFunction.get(); final long timeSinceLastColorProcess = now - mLastProcessLocalColorsTimestamp; final long timeToWait = Math.max(0, PROCESS_LOCAL_COLORS_INTERVAL_MS - timeSinceLastColorProcess); mHandler.postDelayed(() -> { mLastProcessLocalColorsTimestamp = now + timeToWait; mProcessLocalColorsPending.set(false); processLocalColorsInternal(); }, timeToWait); } } /** * Default implementation of the local color extraction. * This will take a screenshot of the whole wallpaper on the main thread. * Then, in a background thread, for each launcher page, for each area that needs color * extraction in this page, creates a sub-bitmap and call {@link WallpaperColors#fromBitmap} * to extract the colors. Every time a launcher page has been processed, call * {@link #notifyLocalColorsChanged} with the color and areas of this page. */ private void processLocalColorsInternal() { if (supportsLocalColorExtraction()) return; float xOffset; float xOffsetStep; float wallpaperDimAmount; int xPage; int xPages; Set areas; EngineWindowPage current; synchronized (mLock) { xOffset = mPendingXOffset; xOffsetStep = mPendingXOffsetStep; wallpaperDimAmount = mWallpaperDimAmount; if (DEBUG) { Log.d(TAG, "processLocalColors " + xOffset + " of step " + xOffsetStep); } if (xOffset % xOffsetStep > MIN_PAGE_ALLOWED_MARGIN || !mSurfaceHolder.getSurface().isValid()) return; int xCurrentPage; if (!validStep(xOffsetStep)) { if (DEBUG) { Log.w(TAG, "invalid offset step " + xOffsetStep); } xOffset = 0; xOffsetStep = 1; xCurrentPage = 0; xPages = 1; } else { xPages = Math.round(1 / xOffsetStep) + 1; xOffsetStep = (float) 1 / (float) xPages; float shrink = (float) (xPages - 1) / (float) xPages; xOffset *= shrink; xCurrentPage = Math.round(xOffset / xOffsetStep); } if (DEBUG) { Log.d(TAG, "xPages " + xPages + " xPage " + xCurrentPage); Log.d(TAG, "xOffsetStep " + xOffsetStep + " xOffset " + xOffset); } float finalXOffsetStep = xOffsetStep; float finalXOffset = xOffset; resetWindowPages(); xPage = xCurrentPage; if (mWindowPages.length == 0 || (mWindowPages.length != xPages)) { mWindowPages = new EngineWindowPage[xPages]; initWindowPages(mWindowPages, finalXOffsetStep); } if (mLocalColorsToAdd.size() != 0) { for (RectF colorArea : mLocalColorsToAdd) { if (!isValid(colorArea)) continue; mLocalColorAreas.add(colorArea); int colorPage = getRectFPage(colorArea, finalXOffsetStep); EngineWindowPage currentPage = mWindowPages[colorPage]; currentPage.setLastUpdateTime(0); currentPage.removeColor(colorArea); } mLocalColorsToAdd.clear(); } if (xPage >= mWindowPages.length) { if (DEBUG) { Log.e(TAG, "error xPage >= mWindowPages.length page: " + xPage); Log.e(TAG, "error on page " + xPage + " out of " + xPages); Log.e(TAG, "error on xOffsetStep " + finalXOffsetStep + " xOffset " + finalXOffset); } xPage = mWindowPages.length - 1; } current = mWindowPages[xPage]; areas = new HashSet<>(current.getAreas()); } updatePage(current, areas, xPage, xPages, wallpaperDimAmount); } @GuardedBy("mLock") private void initWindowPages(EngineWindowPage[] windowPages, float step) { for (int i = 0; i < windowPages.length; i++) { windowPages[i] = new EngineWindowPage(); } mLocalColorAreas.addAll(mLocalColorsToAdd); mLocalColorsToAdd.clear(); for (RectF area: mLocalColorAreas) { if (!isValid(area)) { mLocalColorAreas.remove(area); continue; } int pageNum = getRectFPage(area, step); windowPages[pageNum].addArea(area); } } void updatePage(EngineWindowPage currentPage, Set areas, int pageIndx, int numPages, float wallpaperDimAmount) { // in case the clock is zero, we start with negative time long current = SystemClock.elapsedRealtime() - DEFAULT_UPDATE_SCREENSHOT_DURATION; long lapsed = current - currentPage.getLastUpdateTime(); // Always update the page when the last update time is <= 0 // This is important especially when the device first boots if (lapsed < DEFAULT_UPDATE_SCREENSHOT_DURATION) return; Surface surface = mSurfaceHolder.getSurface(); if (!surface.isValid()) return; boolean widthIsLarger = mSurfaceSize.x > mSurfaceSize.y; int smaller = widthIsLarger ? mSurfaceSize.x : mSurfaceSize.y; float ratio = (float) MIN_BITMAP_SCREENSHOT_WIDTH / (float) smaller; int width = (int) (ratio * mSurfaceSize.x); int height = (int) (ratio * mSurfaceSize.y); if (width <= 0 || height <= 0) { Log.e(TAG, "wrong width and height values of bitmap " + width + " " + height); return; } final String pixelCopySectionName = "WallpaperService#pixelCopy"; final int pixelCopyCount = mPixelCopyCount++; Trace.beginAsyncSection(pixelCopySectionName, pixelCopyCount); Bitmap screenShot = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); final Bitmap finalScreenShot = screenShot; try { // TODO(b/274427458) check if this can be done in the background. PixelCopy.request(surface, screenShot, (res) -> { Trace.endAsyncSection(pixelCopySectionName, pixelCopyCount); if (DEBUG) { Log.d(TAG, "result of pixel copy is: " + (res == PixelCopy.SUCCESS ? "SUCCESS" : "FAILURE")); } if (res != PixelCopy.SUCCESS) { Bitmap lastBitmap = currentPage.getBitmap(); // assign the last bitmap taken for now currentPage.setBitmap(mLastScreenshot); Bitmap lastScreenshot = mLastScreenshot; if (lastScreenshot != null && !Objects.equals(lastBitmap, lastScreenshot)) { updatePageColors( currentPage, areas, pageIndx, numPages, wallpaperDimAmount); } } else { mLastScreenshot = finalScreenShot; currentPage.setBitmap(finalScreenShot); currentPage.setLastUpdateTime(current); updatePageColors( currentPage, areas, pageIndx, numPages, wallpaperDimAmount); } }, mBackgroundHandler); } catch (IllegalArgumentException e) { // this can potentially happen if the surface is invalidated right between the // surface.isValid() check and the PixelCopy operation. // in this case, stop: we'll compute colors on the next processLocalColors call. Log.w(TAG, "Cancelling processLocalColors: exception caught during PixelCopy"); } } // locked by the passed page private void updatePageColors(EngineWindowPage page, Set areas, int pageIndx, int numPages, float wallpaperDimAmount) { if (page.getBitmap() == null) return; if (!mBackgroundHandler.getLooper().isCurrentThread()) { throw new IllegalStateException( "ProcessLocalColors should be called from the background thread"); } Trace.beginSection("WallpaperService#updatePageColors"); if (DEBUG) { Log.d(TAG, "updatePageColorsLocked for page " + pageIndx + " with areas " + page.getAreas().size() + " and bitmap size of " + page.getBitmap().getWidth() + " x " + page.getBitmap().getHeight()); } for (RectF area: areas) { if (area == null) continue; RectF subArea = generateSubRect(area, pageIndx, numPages); Bitmap b = page.getBitmap(); int x = Math.round(b.getWidth() * subArea.left); int y = Math.round(b.getHeight() * subArea.top); int width = Math.round(b.getWidth() * subArea.width()); int height = Math.round(b.getHeight() * subArea.height()); Bitmap target; try { target = Bitmap.createBitmap(b, x, y, width, height); } catch (Exception e) { Log.e(TAG, "Error creating page local color bitmap", e); continue; } WallpaperColors color = WallpaperColors.fromBitmap(target, wallpaperDimAmount); target.recycle(); WallpaperColors currentColor = page.getColors(area); if (DEBUG) { Log.d(TAG, "getting local bitmap area x " + x + " y " + y + " width " + width + " height " + height + " for sub area " + subArea + " and with page " + pageIndx + " of " + numPages); } if (currentColor == null || !color.equals(currentColor)) { page.addWallpaperColors(area, color); if (DEBUG) { Log.d(TAG, "onLocalWallpaperColorsChanged" + " local color callback for area" + area + " for page " + pageIndx + " of " + numPages); } mHandler.post(() -> { try { mConnection.onLocalWallpaperColorsChanged(area, color, mDisplayContext.getDisplayId()); } catch (RemoteException e) { Log.e(TAG, "Error calling Connection.onLocalWallpaperColorsChanged", e); } }); } } Trace.endSection(); } private RectF generateSubRect(RectF in, int pageInx, int numPages) { float minLeft = (float) (pageInx) / (float) (numPages); float maxRight = (float) (pageInx + 1) / (float) (numPages); float left = in.left; float right = in.right; // bound rect if (left < minLeft) left = minLeft; if (right > maxRight) right = maxRight; // scale up the sub area then trim left = (left * (float) numPages) % 1f; right = (right * (float) numPages) % 1f; if (right == 0f) { right = 1f; } return new RectF(left, in.top, right, in.bottom); } @GuardedBy("mLock") private void resetWindowPages() { if (supportsLocalColorExtraction()) return; if (!mResetWindowPages) return; mResetWindowPages = false; for (int i = 0; i < mWindowPages.length; i++) { mWindowPages[i].setLastUpdateTime(0L); } } @GuardedBy("mLock") private int getRectFPage(RectF area, float step) { if (!isValid(area)) return 0; if (!validStep(step)) return 0; int pages = Math.round(1 / step); int page = Math.round(area.centerX() * pages); if (page == pages) return pages - 1; if (page == mWindowPages.length) page = mWindowPages.length - 1; return page; } /** * Add local colors areas of interest * @param regions list of areas * @hide */ public void addLocalColorsAreas(@NonNull List regions) { if (supportsLocalColorExtraction()) return; if (DEBUG) { Log.d(TAG, "addLocalColorsAreas adding local color areas " + regions); } mBackgroundHandler.post(() -> { synchronized (mLock) { mLocalColorsToAdd.addAll(regions); } processLocalColors(); }); } /** * Remove local colors areas of interest if they exist * @param regions list of areas * @hide */ public void removeLocalColorsAreas(@NonNull List regions) { if (supportsLocalColorExtraction()) return; mBackgroundHandler.post(() -> { synchronized (mLock) { float step = mPendingXOffsetStep; mLocalColorsToAdd.removeAll(regions); mLocalColorAreas.removeAll(regions); if (!validStep(step)) { return; } for (int i = 0; i < mWindowPages.length; i++) { for (int j = 0; j < regions.size(); j++) { mWindowPages[i].removeArea(regions.get(j)); } } } }); } // fix the rect to be included within the bounds of the bitmap private Rect fixRect(Bitmap b, Rect r) { r.left = r.left >= r.right || r.left >= b.getWidth() || r.left > 0 ? 0 : r.left; r.right = r.left >= r.right || r.right > b.getWidth() ? b.getWidth() : r.right; return r; } private boolean validStep(float step) { return !Float.isNaN(step) && step > 0f && step <= 1f; } void doCommand(WallpaperCommand cmd) { Bundle result; if (!mDestroyed) { if (COMMAND_FREEZE.equals(cmd.action) || COMMAND_UNFREEZE.equals(cmd.action)) { updateFrozenState(/* frozenRequested= */ !COMMAND_UNFREEZE.equals(cmd.action)); } else if (COMMAND_DISPLAY_SWITCH.equals(cmd.action)) { handleDisplaySwitch(cmd.z == 1 /* startToSwitch */); return; } result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z, cmd.extras, cmd.sync); } else { result = null; } if (cmd.sync) { try { if (DEBUG) Log.v(TAG, "Reporting command complete"); mSession.wallpaperCommandComplete(mWindow.asBinder(), result); } catch (RemoteException e) { } } } private void handleDisplaySwitch(boolean startToSwitch) { if (startToSwitch && mReportedVisible) { // The display may be off/on in a short time when the display is switching. // Keep the visible state until onScreenTurnedOn or !startToSwitch is received, so // the rendering thread can be active to redraw in time when receiving size change. mPreserveVisible = true; mCaller.removeMessages(MSG_REFRESH_VISIBILITY); mCaller.sendMessageDelayed(mCaller.obtainMessage(MSG_REFRESH_VISIBILITY), PRESERVE_VISIBLE_TIMEOUT_MS); } else if (!startToSwitch && mPreserveVisible) { // The switch is finished, so restore to actual visibility. mPreserveVisible = false; mCaller.removeMessages(MSG_REFRESH_VISIBILITY); reportVisibility(false /* forceReport */); } } private void updateFrozenState(boolean frozenRequested) { if (mIWallpaperEngine.mInfo == null // Procees the unfreeze command in case the wallaper became static while // being paused. && frozenRequested) { if (DEBUG) Log.v(TAG, "Ignoring the freeze command for static wallpapers"); return; } mFrozenRequested = frozenRequested; boolean isFrozen = mScreenshotSurfaceControl != null; if (mFrozenRequested == isFrozen) { return; } if (mFrozenRequested) { freeze(); } else { unfreeze(); } } private void freeze() { if (!mReportedVisible || mDestroyed) { // Screenshot can't be taken until visibility is reported to the wallpaper host. return; } if (!showScreenshotOfWallpaper()) { return; } // Prevent a wallpaper host from rendering wallpaper behind a screeshot. doVisibilityChanged(false); // Remember that visibility is requested since it's not guaranteed that // mWindow#dispatchAppVisibility will be called when letterboxed application with // wallpaper background transitions to the Home screen. mVisible = true; } private void unfreeze() { cleanUpScreenshotSurfaceControl(); if (mVisible) { doVisibilityChanged(true); } } private void cleanUpScreenshotSurfaceControl() { // TODO(b/194399558): Add crossfade transition. if (mScreenshotSurfaceControl != null) { new SurfaceControl.Transaction() .remove(mScreenshotSurfaceControl) .show(mBbqSurfaceControl) .apply(); mScreenshotSurfaceControl = null; } } void scaleAndCropScreenshot() { if (mScreenshotSurfaceControl == null) { return; } if (mScreenshotSize.x <= 0 || mScreenshotSize.y <= 0) { Log.w(TAG, "Unexpected screenshot size: " + mScreenshotSize); return; } // Don't scale down and using the same scaling factor for both dimensions to // avoid stretching wallpaper image. float scaleFactor = Math.max(1, Math.max( ((float) mSurfaceSize.x) / mScreenshotSize.x, ((float) mSurfaceSize.y) / mScreenshotSize.y)); int diffX = ((int) (mScreenshotSize.x * scaleFactor)) - mSurfaceSize.x; int diffY = ((int) (mScreenshotSize.y * scaleFactor)) - mSurfaceSize.y; if (DEBUG) { Log.v(TAG, "Adjusting screenshot: scaleFactor=" + scaleFactor + " diffX=" + diffX + " diffY=" + diffY + " mSurfaceSize=" + mSurfaceSize + " mScreenshotSize=" + mScreenshotSize); } new SurfaceControl.Transaction() .setMatrix( mScreenshotSurfaceControl, /* dsdx= */ scaleFactor, /* dtdx= */ 0, /* dtdy= */ 0, /* dsdy= */ scaleFactor) .setWindowCrop( mScreenshotSurfaceControl, new Rect( /* left= */ diffX / 2, /* top= */ diffY / 2, /* right= */ diffX / 2 + mScreenshotSize.x, /* bottom= */ diffY / 2 + mScreenshotSize.y)) .setPosition(mScreenshotSurfaceControl, -diffX / 2, -diffY / 2) .apply(); } private boolean showScreenshotOfWallpaper() { if (mDestroyed || mSurfaceControl == null || !mSurfaceControl.isValid()) { if (DEBUG) Log.v(TAG, "Failed to screenshot wallpaper: surface isn't valid"); return false; } final Rect bounds = new Rect(0, 0, mSurfaceSize.x, mSurfaceSize.y); if (bounds.isEmpty()) { Log.w(TAG, "Failed to screenshot wallpaper: surface bounds are empty"); return false; } if (mScreenshotSurfaceControl != null) { Log.e(TAG, "Screenshot is unexpectedly not null"); // Destroying previous screenshot since it can have different size. cleanUpScreenshotSurfaceControl(); } ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = ScreenCapture.captureLayers( new ScreenCapture.LayerCaptureArgs.Builder(mSurfaceControl) // Needed because SurfaceFlinger#validateScreenshotPermissions // uses this parameter to check whether a caller only attempts // to screenshot itself when call doesn't come from the system. .setUid(Process.myUid()) .setChildrenOnly(false) .setSourceCrop(bounds) .build()); if (screenshotBuffer == null) { Log.w(TAG, "Failed to screenshot wallpaper: screenshotBuffer is null"); return false; } final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); SurfaceControl.Transaction t = new SurfaceControl.Transaction(); // TODO(b/194399558): Add crossfade transition. mScreenshotSurfaceControl = new SurfaceControl.Builder() .setName("Wallpaper snapshot for engine " + this) .setFormat(hardwareBuffer.getFormat()) .setParent(mSurfaceControl) .setSecure(screenshotBuffer.containsSecureLayers()) .setCallsite("WallpaperService.Engine.showScreenshotOfWallpaper") .setBLASTLayer() .build(); mScreenshotSize.set(mSurfaceSize.x, mSurfaceSize.y); t.setBuffer(mScreenshotSurfaceControl, hardwareBuffer); t.setColorSpace(mScreenshotSurfaceControl, screenshotBuffer.getColorSpace()); // Place on top everything else. t.setLayer(mScreenshotSurfaceControl, Integer.MAX_VALUE); t.show(mScreenshotSurfaceControl); t.hide(mBbqSurfaceControl); t.apply(); return true; } void reportSurfaceDestroyed() { if (mSurfaceCreated) { mSurfaceCreated = false; mSurfaceHolder.ungetCallbacks(); SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); if (callbacks != null) { for (SurfaceHolder.Callback c : callbacks) { c.surfaceDestroyed(mSurfaceHolder); } } if (DEBUG) Log.v(TAG, "onSurfaceDestroyed(" + mSurfaceHolder + "): " + this); onSurfaceDestroyed(mSurfaceHolder); } } /** * @hide */ @VisibleForTesting public void detach() { if (mDestroyed) { return; } AnimationHandler.removeRequestor(this); mDestroyed = true; if (mIWallpaperEngine != null && mIWallpaperEngine.mDisplayManager != null) { mIWallpaperEngine.mDisplayManager.unregisterDisplayListener(mDisplayListener); } if (mVisible) { mVisible = false; if (DEBUG) Log.v(TAG, "onVisibilityChanged(false): " + this); onVisibilityChanged(false); } reportSurfaceDestroyed(); if (DEBUG) Log.v(TAG, "onDestroy(): " + this); onDestroy(); if (mCreated) { try { if (DEBUG) Log.v(TAG, "Removing window and destroying surface " + mSurfaceHolder.getSurface() + " of: " + this); if (mInputEventReceiver != null) { mInputEventReceiver.dispose(); mInputEventReceiver = null; } mSession.remove(mWindow.asBinder()); } catch (RemoteException e) { } mSurfaceHolder.mSurface.release(); if (mBlastBufferQueue != null) { mBlastBufferQueue.destroy(); mBlastBufferQueue = null; } if (mBbqSurfaceControl != null) { new SurfaceControl.Transaction().remove(mBbqSurfaceControl).apply(); mBbqSurfaceControl = null; } mCreated = false; } if (mSurfaceControl != null) { mSurfaceControl.release(); mSurfaceControl = null; mRelayoutResult = null; } } private final DisplayListener mDisplayListener = new DisplayListener() { @Override public void onDisplayChanged(int displayId) { if (mDisplay.getDisplayId() == displayId) { boolean forceReport = mIsWearOs && mDisplay.getState() != Display.STATE_DOZE_SUSPEND; reportVisibility(forceReport); } } @Override public void onDisplayRemoved(int displayId) {} @Override public void onDisplayAdded(int displayId) {} }; private Surface getOrCreateBLASTSurface(int width, int height, int format) { Surface ret = null; if (mBlastBufferQueue == null) { mBlastBufferQueue = new BLASTBufferQueue("Wallpaper", mBbqSurfaceControl, width, height, format); // We only return the Surface the first time, as otherwise // it hasn't changed and there is no need to update. ret = mBlastBufferQueue.createSurface(); } else { mBlastBufferQueue.update(mBbqSurfaceControl, width, height, format); } return ret; } } /** * Returns a Looper which messages such as {@link WallpaperService#DO_ATTACH}, * {@link WallpaperService#DO_DETACH} etc. are sent to. * By default, returns the process's main looper. * @hide */ @NonNull public Looper onProvideEngineLooper() { return super.getMainLooper(); } private boolean isValid(RectF area) { if (area == null) return false; boolean valid = area.bottom > area.top && area.left < area.right && LOCAL_COLOR_BOUNDS.contains(area); return valid; } private boolean inRectFRange(float number) { return number >= 0f && number <= 1f; } class IWallpaperEngineWrapper extends IWallpaperEngine.Stub implements HandlerCaller.Callback { private final HandlerCaller mCaller; final IWallpaperConnection mConnection; final IBinder mWindowToken; final int mWindowType; final boolean mIsPreview; final AtomicInteger mPendingResizeCount = new AtomicInteger(); boolean mReportDraw; boolean mShownReported; int mReqWidth; int mReqHeight; final Rect mDisplayPadding = new Rect(); final int mDisplayId; final DisplayManager mDisplayManager; final Display mDisplay; final WallpaperManager mWallpaperManager; @Nullable final WallpaperInfo mInfo; Engine mEngine; @SetWallpaperFlags int mWhich; IWallpaperEngineWrapper(WallpaperService service, IWallpaperConnection conn, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding, int displayId, @SetWallpaperFlags int which, @Nullable WallpaperInfo info) { mWallpaperManager = getSystemService(WallpaperManager.class); mCaller = new HandlerCaller(service, service.onProvideEngineLooper(), this, true); mConnection = conn; mWindowToken = windowToken; mWindowType = windowType; mIsPreview = isPreview; mReqWidth = reqWidth; mReqHeight = reqHeight; mDisplayPadding.set(padding); mDisplayId = displayId; mWhich = which; mInfo = info; // Create a display context before onCreateEngine. mDisplayManager = getSystemService(DisplayManager.class); mDisplay = mDisplayManager.getDisplay(mDisplayId); if (mDisplay == null) { // Ignore this engine. throw new IllegalArgumentException("Cannot find display with id" + mDisplayId); } Message msg = mCaller.obtainMessage(DO_ATTACH); mCaller.sendMessage(msg); } public void setDesiredSize(int width, int height) { Message msg = mCaller.obtainMessageII(DO_SET_DESIRED_SIZE, width, height); mCaller.sendMessage(msg); } public void setDisplayPadding(Rect padding) { Message msg = mCaller.obtainMessageO(DO_SET_DISPLAY_PADDING, padding); mCaller.sendMessage(msg); } public void setVisibility(boolean visible) { Message msg = mCaller.obtainMessageI(MSG_VISIBILITY_CHANGED, visible ? 1 : 0); mCaller.sendMessage(msg); } @Override public void setWallpaperFlags(@SetWallpaperFlags int which) { if (which == mWhich) { return; } mWhich = which; Message msg = mCaller.obtainMessageI(MSG_WALLPAPER_FLAGS_CHANGED, which); mCaller.sendMessage(msg); } @Override public void setInAmbientMode(boolean inAmbientDisplay, long animationDuration) throws RemoteException { Message msg = mCaller.obtainMessageIO(DO_IN_AMBIENT_MODE, inAmbientDisplay ? 1 : 0, animationDuration); mCaller.sendMessage(msg); } public void dispatchPointer(MotionEvent event) { if (mEngine != null) { mEngine.dispatchPointer(event); } else { event.recycle(); } } public void dispatchWallpaperCommand(String action, int x, int y, int z, Bundle extras) { if (mEngine != null) { mEngine.mWindow.dispatchWallpaperCommand(action, x, y, z, extras, false); } } public void setZoomOut(float scale) { Message msg = mCaller.obtainMessageI(MSG_ZOOM, Float.floatToIntBits(scale)); mCaller.sendMessage(msg); } public void reportShown() { if (mEngine == null) { Log.i(TAG, "Can't report null engine as shown."); return; } if (mEngine.mDestroyed) { Log.i(TAG, "Engine was destroyed before we could draw."); return; } if (!mShownReported) { mShownReported = true; Trace.beginSection("WPMS.mConnection.engineShown"); try { mConnection.engineShown(this); Log.d(TAG, "Wallpaper has updated the surface:" + mInfo); } catch (RemoteException e) { Log.w(TAG, "Wallpaper host disappeared", e); } Trace.endSection(); } } public void requestWallpaperColors() { Message msg = mCaller.obtainMessage(MSG_REQUEST_WALLPAPER_COLORS); mCaller.sendMessage(msg); } public void addLocalColorsAreas(List regions) { mEngine.addLocalColorsAreas(regions); } public void removeLocalColorsAreas(List regions) { mEngine.removeLocalColorsAreas(regions); } public void applyDimming(float dimAmount) throws RemoteException { Message msg = mCaller.obtainMessageI(MSG_UPDATE_DIMMING, Float.floatToIntBits(dimAmount)); mCaller.sendMessage(msg); } public void destroy() { Message msg = mCaller.obtainMessage(DO_DETACH); mCaller.getHandler().removeCallbacksAndMessages(null); mCaller.sendMessage(msg); } public void resizePreview(Rect position) { Message msg = mCaller.obtainMessageO(MSG_RESIZE_PREVIEW, position); mCaller.sendMessage(msg); } @Nullable public SurfaceControl mirrorSurfaceControl() { return mEngine == null ? null : SurfaceControl.mirrorSurface(mEngine.mSurfaceControl); } private void doAttachEngine() { Trace.beginSection("WPMS.onCreateEngine"); Engine engine = onCreateEngine(); Trace.endSection(); mEngine = engine; Trace.beginSection("WPMS.mConnection.attachEngine-" + mDisplayId); try { mConnection.attachEngine(this, mDisplayId); } catch (RemoteException e) { engine.detach(); Log.w(TAG, "Wallpaper host disappeared", e); return; } catch (IllegalStateException e) { Log.w(TAG, "Connector instance already destroyed, " + "can't attach engine to non existing connector", e); return; } finally { Trace.endSection(); } Trace.beginSection("WPMS.engine.attach"); engine.attach(this); Trace.endSection(); } private void doDetachEngine() { // Some wallpapers will not trigger the rendering threads of the remaining engines even // if they are visible, so we need to toggle the state to get their attention. if (mEngine != null && !mEngine.mDestroyed) { mEngine.detach(); synchronized (mActiveEngines) { for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { if (engineWrapper.mEngine != null && engineWrapper.mEngine.mVisible) { engineWrapper.mEngine.doVisibilityChanged(false); engineWrapper.mEngine.doVisibilityChanged(true); } } } } } public void updateScreenTurningOn(boolean isScreenTurningOn) { Message msg = mCaller.obtainMessageBO(MSG_UPDATE_SCREEN_TURNING_ON, isScreenTurningOn, null); mCaller.sendMessage(msg); } public void onScreenTurningOn() throws RemoteException { updateScreenTurningOn(true); } public void onScreenTurnedOn() throws RemoteException { updateScreenTurningOn(false); } @Override public void executeMessage(Message message) { switch (message.what) { case DO_ATTACH: { Trace.beginSection("WPMS.DO_ATTACH"); doAttachEngine(); Trace.endSection(); return; } case DO_DETACH: { Trace.beginSection("WPMS.DO_DETACH"); doDetachEngine(); Trace.endSection(); return; } case DO_SET_DESIRED_SIZE: { mEngine.doDesiredSizeChanged(message.arg1, message.arg2); return; } case DO_SET_DISPLAY_PADDING: { mEngine.doDisplayPaddingChanged((Rect) message.obj); return; } case DO_IN_AMBIENT_MODE: { mEngine.doAmbientModeChanged(message.arg1 != 0, (Long) message.obj); return; } case MSG_UPDATE_SURFACE: mEngine.updateSurface(true, false, false); break; case MSG_ZOOM: mEngine.setZoom(Float.intBitsToFloat(message.arg1)); break; case MSG_UPDATE_DIMMING: mEngine.updateWallpaperDimming(Float.intBitsToFloat(message.arg1)); break; case MSG_RESIZE_PREVIEW: mEngine.resizePreview((Rect) message.obj); break; case MSG_VISIBILITY_CHANGED: if (DEBUG) Log.v(TAG, "Visibility change in " + mEngine + ": " + message.arg1); mEngine.doVisibilityChanged(message.arg1 != 0); break; case MSG_REFRESH_VISIBILITY: mEngine.mPreserveVisible = false; mEngine.reportVisibility(false /* forceReport */); break; case MSG_UPDATE_SCREEN_TURNING_ON: if (DEBUG) { Log.v(TAG, message.arg1 != 0 ? "Screen turning on" : "Screen turned on"); } mEngine.onScreenTurningOnChanged(/* isScreenTurningOn= */ message.arg1 != 0); break; case MSG_WALLPAPER_OFFSETS: { mEngine.doOffsetsChanged(true); } break; case MSG_WALLPAPER_COMMAND: { WallpaperCommand cmd = (WallpaperCommand)message.obj; mEngine.doCommand(cmd); } break; case MSG_WINDOW_RESIZED: { handleResized((MergedConfiguration) message.obj, message.arg1 != 0); } break; case MSG_WINDOW_MOVED: { // Do nothing. What does it mean for a Wallpaper to move? } break; case MSG_TOUCH_EVENT: { boolean skip = false; MotionEvent ev = (MotionEvent)message.obj; if (ev.getAction() == MotionEvent.ACTION_MOVE) { synchronized (mEngine.mLock) { if (mEngine.mPendingMove == ev) { mEngine.mPendingMove = null; } else { // this is not the motion event we are looking for.... skip = true; } } } if (!skip) { if (DEBUG) Log.v(TAG, "Delivering touch event: " + ev); mEngine.onTouchEvent(ev); } ev.recycle(); } break; case MSG_REQUEST_WALLPAPER_COLORS: { if (mConnection == null) { break; } try { WallpaperColors colors = mEngine.onComputeColors(); mEngine.setPrimaryWallpaperColors(colors); mConnection.onWallpaperColorsChanged(colors, mDisplayId); } catch (RemoteException e) { // Connection went away, nothing to do in here. } } break; case MSG_REPORT_SHOWN: { Trace.beginSection("WPMS.MSG_REPORT_SHOWN"); reportShown(); Trace.endSection(); } break; case MSG_WALLPAPER_FLAGS_CHANGED: { mEngine.onWallpaperFlagsChanged(message.arg1); } break; default : Log.w(TAG, "Unknown message type " + message.what); } } /** * In general this performs relayout for IWindow#resized. If there are several pending * (in the message queue) MSG_WINDOW_RESIZED from server side, only the last one will be * handled (ignore intermediate states). Note that this procedure cannot be skipped if the * configuration is not changed because this is also used to dispatch insets changes. */ private void handleResized(MergedConfiguration config, boolean reportDraw) { // The config can be null when retrying for a changed config from relayout, otherwise // it is from IWindow#resized which always sends non-null config. final int pendingCount = config != null ? mPendingResizeCount.decrementAndGet() : -1; if (reportDraw) { mReportDraw = true; } if (pendingCount > 0) { if (DEBUG) { Log.d(TAG, "Skip outdated resize, bounds=" + config.getMergedConfiguration().windowConfiguration.getMaxBounds() + " pendingCount=" + pendingCount); } return; } if (config != null) { if (DEBUG) { Log.d(TAG, "Update config from resized, bounds=" + config.getMergedConfiguration().windowConfiguration.getMaxBounds()); } mEngine.mMergedConfiguration.setTo(config); } mEngine.updateSurface(true /* forceRelayout */, false /* forceReport */, mReportDraw); mReportDraw = false; mEngine.doOffsetsChanged(true); mEngine.scaleAndCropScreenshot(); } } /** * Implements the internal {@link IWallpaperService} interface to convert * incoming calls to it back to calls on an {@link WallpaperService}. */ class IWallpaperServiceWrapper extends IWallpaperService.Stub { private final WallpaperService mTarget; public IWallpaperServiceWrapper(WallpaperService context) { mTarget = context; } @Override public void attach(IWallpaperConnection conn, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding, int displayId, @SetWallpaperFlags int which, @Nullable WallpaperInfo info) { Trace.beginSection("WPMS.ServiceWrapper.attach"); IWallpaperEngineWrapper engineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken, windowType, isPreview, reqWidth, reqHeight, padding, displayId, which, info); synchronized (mActiveEngines) { mActiveEngines.put(windowToken, engineWrapper); } if (DEBUG) { Slog.v(TAG, "IWallpaperServiceWrapper Attaching window token " + windowToken); } Trace.endSection(); } @Override public void detach(IBinder windowToken) { IWallpaperEngineWrapper engineWrapper; synchronized (mActiveEngines) { engineWrapper = mActiveEngines.remove(windowToken); } if (engineWrapper == null) { Log.w(TAG, "Engine for window token " + windowToken + " already detached"); return; } if (DEBUG) { Slog.v(TAG, "IWallpaperServiceWrapper Detaching window token " + windowToken); } engineWrapper.destroy(); } } @Override public void onCreate() { Trace.beginSection("WPMS.onCreate"); mBackgroundThread = new HandlerThread("DefaultWallpaperLocalColorExtractor"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); mIsWearOs = getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); super.onCreate(); Trace.endSection(); } @Override public void onDestroy() { Trace.beginSection("WPMS.onDestroy"); super.onDestroy(); synchronized (mActiveEngines) { for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { engineWrapper.destroy(); } mActiveEngines.clear(); } if (mBackgroundThread != null) { // onDestroy might be called without a previous onCreate if WallpaperService was // instantiated manually. While this is a misuse of the API, some things break // if here we don't take into consideration this scenario. mBackgroundThread.quitSafely(); } Trace.endSection(); } /** * Implement to return the implementation of the internal accessibility * service interface. Subclasses should not override. */ @Override public final IBinder onBind(Intent intent) { return new IWallpaperServiceWrapper(this); } /** * Must be implemented to return a new instance of the wallpaper's engine. * Note that multiple instances may be active at the same time, such as * when the wallpaper is currently set as the active wallpaper and the user * is in the wallpaper picker viewing a preview of it as well. */ @MainThread public abstract Engine onCreateEngine(); @Override protected void dump(FileDescriptor fd, PrintWriter out, String[] args) { out.print("State of wallpaper "); out.print(this); out.println(":"); synchronized (mActiveEngines) { for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { Engine engine = engineWrapper.mEngine; if (engine == null) { Slog.w(TAG, "Engine for wrapper " + engineWrapper + " not attached"); continue; } out.print(" Engine "); out.print(engine); out.println(":"); engine.dump(" ", fd, out, args); } } } }