| /* |
| * Copyright (C) 2020 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.wallpaper.util; |
| |
| 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 android.app.WallpaperColors; |
| import android.app.WallpaperManager; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.graphics.Matrix; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.service.wallpaper.IWallpaperConnection; |
| import android.service.wallpaper.IWallpaperEngine; |
| import android.service.wallpaper.IWallpaperService; |
| import android.util.Log; |
| import android.view.Display; |
| import android.view.SurfaceControl; |
| import android.view.SurfaceHolder; |
| import android.view.SurfaceHolder.Callback; |
| import android.view.SurfaceView; |
| import android.view.View; |
| import android.view.WindowManager.LayoutParams; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Implementation of {@link IWallpaperConnection} that handles communication with a |
| * {@link android.service.wallpaper.WallpaperService} |
| */ |
| public class WallpaperConnection extends IWallpaperConnection.Stub implements ServiceConnection { |
| |
| /** |
| * Defines different possible scenarios for which we need to dispatch a command from picker to |
| * the wallpaper. |
| */ |
| public enum WhichPreview { |
| /** |
| * Represents the case when we preview a currently applied wallpaper (home/lock) simply |
| * by tapping on it. |
| */ |
| PREVIEW_CURRENT(0), |
| /** |
| * Represents the case when we are editing the currently applied wallpaper. |
| */ |
| EDIT_CURRENT(1), |
| /** |
| * Represents the case when we are editing a wallpaper that's not currently applied. |
| */ |
| EDIT_NON_CURRENT(2); |
| |
| private final int mValue; |
| |
| WhichPreview(int value) { |
| this.mValue = value; |
| } |
| |
| public int getValue() { |
| return mValue; |
| } |
| } |
| |
| /** |
| * Returns whether live preview is available in framework. |
| */ |
| public static boolean isPreviewAvailable() { |
| try { |
| return IWallpaperEngine.class.getMethod("mirrorSurfaceControl") != null; |
| } catch (NoSuchMethodException | SecurityException e) { |
| return false; |
| } |
| } |
| |
| private static final String TAG = "WallpaperConnection"; |
| private static final Looper sMainLooper = Looper.getMainLooper(); |
| private final Context mContext; |
| private final Intent mIntent; |
| private final List<SurfaceControl> mMirrorSurfaceControls = new ArrayList<>(); |
| private WallpaperConnectionListener mListener; |
| private SurfaceView mContainerView; |
| private SurfaceView mSecondContainerView; |
| private IWallpaperService mService; |
| @Nullable private IWallpaperEngine mEngine; |
| @Nullable private Point mDisplayMetrics; |
| private boolean mConnected; |
| private boolean mIsVisible; |
| private boolean mIsEngineVisible; |
| private boolean mEngineReady; |
| private boolean mDestroyed; |
| private int mDestinationFlag; |
| private WhichPreview mWhichPreview; |
| |
| /** |
| * @param intent used to bind the wallpaper service |
| * @param context Context used to start and bind the live wallpaper service |
| * @param listener if provided, it'll be notified of connection/disconnection events |
| * @param containerView SurfaceView that will display the wallpaper |
| */ |
| public WallpaperConnection(Intent intent, Context context, |
| @Nullable WallpaperConnectionListener listener, @NonNull SurfaceView containerView, |
| WhichPreview preview) { |
| this(intent, context, listener, containerView, null, null, |
| preview); |
| } |
| |
| /** |
| * @param intent used to bind the wallpaper service |
| * @param context Context used to start and bind the live wallpaper service |
| * @param listener if provided, it'll be notified of connection/disconnection events |
| * @param containerView SurfaceView that will display the wallpaper |
| * @param secondaryContainerView optional SurfaceView that will display a second, mirrored |
| * version of the wallpaper |
| * @param destinationFlag one of WallpaperManager.FLAG_SYSTEM, WallpaperManager.FLAG_LOCK |
| * indicating for which screen we're previewing the wallpaper, or null if |
| * unknown |
| */ |
| public WallpaperConnection(Intent intent, Context context, |
| @Nullable WallpaperConnectionListener listener, @NonNull SurfaceView containerView, |
| @Nullable SurfaceView secondaryContainerView, |
| @Nullable @WallpaperManager.SetWallpaperFlags Integer destinationFlag, |
| WhichPreview preview) { |
| mContext = context.getApplicationContext(); |
| mIntent = intent; |
| mListener = listener; |
| mContainerView = containerView; |
| mSecondContainerView = secondaryContainerView; |
| mDestinationFlag = destinationFlag == null ? WallpaperManager.FLAG_SYSTEM : destinationFlag; |
| mWhichPreview = preview; |
| } |
| |
| /** |
| * Bind the Service for this connection. |
| */ |
| public boolean connect() { |
| if (mDestroyed) { |
| throw new IllegalStateException("Cannot connect on a destroyed WallpaperConnection"); |
| } |
| synchronized (this) { |
| if (mConnected) { |
| return true; |
| } |
| if (!mContext.bindService(mIntent, this, |
| Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT |
| | Context.BIND_ALLOW_ACTIVITY_STARTS)) { |
| return false; |
| } |
| |
| mConnected = true; |
| } |
| |
| if (mListener != null) { |
| mListener.onConnected(); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Disconnect and destroy the WallpaperEngine for this connection. |
| */ |
| public void disconnect() { |
| synchronized (this) { |
| mConnected = false; |
| if (mEngine != null) { |
| try { |
| mEngine.destroy(); |
| for (SurfaceControl control : mMirrorSurfaceControls) { |
| control.release(); |
| } |
| mMirrorSurfaceControls.clear(); |
| } catch (RemoteException e) { |
| // Ignore |
| } |
| mEngine = null; |
| } |
| try { |
| mContext.unbindService(this); |
| } catch (IllegalArgumentException e) { |
| Log.i(TAG, "Can't unbind wallpaper service. " |
| + "It might have crashed, just ignoring."); |
| } |
| mService = null; |
| } |
| if (mListener != null) { |
| mListener.onDisconnected(); |
| } |
| } |
| |
| /** |
| * Clean up references on this WallpaperConnection. |
| * After calling this method, {@link #connect()} cannot be called again. |
| */ |
| public void destroy() { |
| disconnect(); |
| mContainerView = null; |
| mSecondContainerView = null; |
| mListener = null; |
| mDestroyed = true; |
| } |
| |
| /** |
| * @see ServiceConnection#onServiceConnected(ComponentName, IBinder) |
| */ |
| public void onServiceConnected(ComponentName name, IBinder service) { |
| if (mContainerView == null) { |
| return; |
| } |
| mService = IWallpaperService.Stub.asInterface(service); |
| if (mContainerView.getDisplay() == null) { |
| mContainerView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { |
| @Override |
| public void onViewAttachedToWindow(View v) { |
| attachConnection(v.getDisplay().getDisplayId()); |
| mContainerView.removeOnAttachStateChangeListener(this); |
| } |
| |
| @Override |
| public void onViewDetachedFromWindow(View v) {} |
| }); |
| } else { |
| attachConnection(mContainerView.getDisplay().getDisplayId()); |
| } |
| } |
| |
| @Override |
| public void onLocalWallpaperColorsChanged(RectF area, |
| WallpaperColors colors, int displayId) { |
| |
| } |
| |
| /** |
| * @see ServiceConnection#onServiceDisconnected(ComponentName) |
| */ |
| public void onServiceDisconnected(ComponentName name) { |
| mService = null; |
| mEngine = null; |
| Log.w(TAG, "Wallpaper service gone: " + name); |
| } |
| |
| /** |
| * @see IWallpaperConnection#attachEngine(IWallpaperEngine, int) |
| */ |
| public void attachEngine(IWallpaperEngine engine, int displayId) { |
| synchronized (this) { |
| if (mConnected) { |
| mEngine = engine; |
| if (mIsVisible) { |
| setEngineVisibility(true); |
| } |
| |
| try { |
| Point displayMetrics = getDisplayMetrics(); |
| // Reset the live wallpaper preview with the correct screen dimensions. It is |
| // a known issue that the wallpaper service maybe get the Activity window size |
| // which may differ from the actual physical device screen size, e.g. when in |
| // 2-pane mode. |
| // TODO b/262750854 Fix wallpaper service to get the actual physical device |
| // screen size instead of the window size that might be smaller when in |
| // 2-pane mode. |
| mEngine.resizePreview(new Rect(0, 0, displayMetrics.x, displayMetrics.y)); |
| // Some wallpapers don't trigger #onWallpaperColorsChanged from remote. |
| // Requesting wallpaper color here to ensure the #onWallpaperColorsChanged |
| // would get called. |
| mEngine.requestWallpaperColors(); |
| } catch (RemoteException | NullPointerException e) { |
| Log.w(TAG, "Failed calling WallpaperEngine APIs", e); |
| } |
| } else { |
| try { |
| engine.destroy(); |
| } catch (RemoteException e) { |
| // Ignore |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the engine handled by this WallpaperConnection |
| */ |
| @Nullable |
| public IWallpaperEngine getEngine() { |
| return mEngine; |
| } |
| |
| /** |
| * @see IWallpaperConnection#setWallpaper(String) |
| */ |
| public ParcelFileDescriptor setWallpaper(String name) { |
| return null; |
| } |
| |
| @Override |
| public void onWallpaperColorsChanged(WallpaperColors colors, int displayId) { |
| if (mContainerView != null) { |
| mContainerView.post(() -> { |
| if (mListener != null) { |
| mListener.onWallpaperColorsChanged(colors, displayId); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void engineShown(IWallpaperEngine engine) { |
| mEngineReady = true; |
| Bundle bundle = new Bundle(); |
| bundle.putInt("which_preview", mWhichPreview.getValue()); |
| try { |
| engine.dispatchWallpaperCommand("android.wallpaper.previewinfo", 0, 0, 0, bundle); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error dispatching wallpaper command: " + mWhichPreview.toString()); |
| } |
| if (mContainerView != null) { |
| mContainerView.post(() -> reparentWallpaperSurface(mContainerView)); |
| } |
| if (mSecondContainerView != null) { |
| mSecondContainerView.post(() -> reparentWallpaperSurface(mSecondContainerView)); |
| } |
| if (mContainerView != null) { |
| mContainerView.post(() -> { |
| if (mListener != null) { |
| mListener.onEngineShown(); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Returns true if the wallpaper engine has been initialized. |
| */ |
| public boolean isEngineReady() { |
| return mEngineReady; |
| } |
| |
| /** |
| * Sets the engine's visibility. |
| */ |
| public void setVisibility(boolean visible) { |
| synchronized (this) { |
| mIsVisible = visible; |
| setEngineVisibility(visible); |
| } |
| } |
| |
| |
| /** |
| * Set the {@link android.app.WallpaperManager.SetWallpaperFlags} to the Engine to indicate |
| * which screen it's being applied/previewed to. |
| */ |
| public void setWallpaperFlags(@WallpaperManager.SetWallpaperFlags int wallpaperFlags) |
| throws RemoteException { |
| if (mEngine != null && mEngineReady) { |
| mEngine.setWallpaperFlags(wallpaperFlags); |
| } |
| } |
| |
| private void attachConnection(int displayId) { |
| try { |
| try { |
| Method preUMethod = mService.getClass().getMethod("attach", |
| IWallpaperConnection.class, IBinder.class, int.class, boolean.class, |
| int.class, int.class, Rect.class, int.class); |
| preUMethod.invoke(mService, this, mContainerView.getWindowToken(), |
| LayoutParams.TYPE_APPLICATION_MEDIA, true, mContainerView.getWidth(), |
| mContainerView.getHeight(), new Rect(0, 0, 0, 0), displayId); |
| } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { |
| Log.d(TAG, "IWallpaperService#attach method without which argument not available, " |
| + "will use newer version"); |
| // Let's try the new attach method that takes "which" argument |
| mService.attach(this, mContainerView.getWindowToken(), |
| LayoutParams.TYPE_APPLICATION_MEDIA, true, mContainerView.getWidth(), |
| mContainerView.getHeight(), new Rect(0, 0, 0, 0), displayId, |
| mDestinationFlag, null); |
| } |
| } catch (RemoteException e) { |
| Log.w(TAG, "Failed attaching wallpaper; clearing", e); |
| } |
| } |
| |
| private void setEngineVisibility(boolean visible) { |
| if (mEngine != null && visible != mIsEngineVisible) { |
| try { |
| mEngine.setVisibility(visible); |
| mIsEngineVisible = visible; |
| } catch (RemoteException e) { |
| Log.w(TAG, "Failure setting wallpaper visibility ", e); |
| } |
| } |
| } |
| |
| private void reparentWallpaperSurface(SurfaceView parentSurface) { |
| if (parentSurface == null) { |
| return; |
| } |
| synchronized (this) { |
| if (mEngine == null) { |
| Log.i(TAG, "Engine is null, was the service disconnected?"); |
| return; |
| } |
| } |
| if (parentSurface.getSurfaceControl() != null) { |
| mirrorAndReparent(parentSurface); |
| } else { |
| Log.d(TAG, "SurfaceView not initialized yet, adding callback"); |
| parentSurface.getHolder().addCallback(new Callback() { |
| @Override |
| public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { |
| |
| } |
| |
| @Override |
| public void surfaceCreated(SurfaceHolder surfaceHolder) { |
| mirrorAndReparent(parentSurface); |
| parentSurface.getHolder().removeCallback(this); |
| } |
| |
| @Override |
| public void surfaceDestroyed(SurfaceHolder surfaceHolder) { |
| |
| } |
| }); |
| } |
| } |
| |
| private void mirrorAndReparent(SurfaceView parentSurface) { |
| IWallpaperEngine engine; |
| synchronized (this) { |
| if (mEngine == null) { |
| Log.i(TAG, "Engine is null, was the service disconnected?"); |
| return; |
| } |
| engine = mEngine; |
| } |
| try { |
| SurfaceControl parentSC = parentSurface.getSurfaceControl(); |
| SurfaceControl wallpaperMirrorSC = engine.mirrorSurfaceControl(); |
| if (wallpaperMirrorSC == null) { |
| return; |
| } |
| float[] values = getScale(parentSurface); |
| try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) { |
| t.setMatrix(wallpaperMirrorSC, values[MSCALE_X], values[MSKEW_Y], |
| values[MSKEW_X], values[MSCALE_Y]); |
| t.reparent(wallpaperMirrorSC, parentSC); |
| t.show(wallpaperMirrorSC); |
| t.apply(); |
| } |
| synchronized (this) { |
| mMirrorSurfaceControls.add(wallpaperMirrorSC); |
| } |
| } catch (RemoteException | NullPointerException e) { |
| Log.e(TAG, "Couldn't reparent wallpaper surface", e); |
| } |
| } |
| |
| private float[] getScale(SurfaceView parentSurface) { |
| Matrix m = new Matrix(); |
| float[] values = new float[9]; |
| Rect surfacePosition = parentSurface.getHolder().getSurfaceFrame(); |
| Point displayMetrics = getDisplayMetrics(); |
| m.postScale(((float) surfacePosition.width()) / displayMetrics.x, |
| ((float) surfacePosition.height()) / displayMetrics.y); |
| m.getValues(values); |
| return values; |
| } |
| |
| /** |
| * Get display metrics. Only call this when the display is attached to the window. |
| */ |
| private Point getDisplayMetrics() { |
| if (mDisplayMetrics != null) { |
| return mDisplayMetrics; |
| } |
| ScreenSizeCalculator screenSizeCalculator = ScreenSizeCalculator.getInstance(); |
| Display display = mContainerView.getDisplay(); |
| if (display == null) { |
| throw new NullPointerException( |
| "Display is null due to the view not currently attached to a window."); |
| } |
| mDisplayMetrics = screenSizeCalculator.getScreenSize(display); |
| return mDisplayMetrics; |
| } |
| |
| /** |
| * Interface to be notified of connect/disconnect events from {@link WallpaperConnection} |
| */ |
| public interface WallpaperConnectionListener { |
| /** |
| * Called after the Wallpaper service has been bound. |
| */ |
| default void onConnected() {} |
| |
| /** |
| * Called after the Wallpaper engine has been terminated and the service has been unbound. |
| */ |
| default void onDisconnected() {} |
| |
| /** |
| * Called after the wallpaper has been rendered for the first time. |
| */ |
| default void onEngineShown() {} |
| |
| /** |
| * Called after the wallpaper color is available or updated. |
| */ |
| default void onWallpaperColorsChanged(WallpaperColors colors, int displayId) {} |
| } |
| } |