1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.wallpaper.util;
17 
18 import static android.graphics.Matrix.MSCALE_X;
19 import static android.graphics.Matrix.MSCALE_Y;
20 import static android.graphics.Matrix.MSKEW_X;
21 import static android.graphics.Matrix.MSKEW_Y;
22 
23 import android.app.WallpaperColors;
24 import android.app.WallpaperManager;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.ServiceConnection;
29 import android.graphics.Matrix;
30 import android.graphics.Point;
31 import android.graphics.Rect;
32 import android.graphics.RectF;
33 import android.os.Bundle;
34 import android.os.IBinder;
35 import android.os.Looper;
36 import android.os.ParcelFileDescriptor;
37 import android.os.RemoteException;
38 import android.service.wallpaper.IWallpaperConnection;
39 import android.service.wallpaper.IWallpaperEngine;
40 import android.service.wallpaper.IWallpaperService;
41 import android.util.Log;
42 import android.view.Display;
43 import android.view.SurfaceControl;
44 import android.view.SurfaceHolder;
45 import android.view.SurfaceHolder.Callback;
46 import android.view.SurfaceView;
47 import android.view.View;
48 import android.view.WindowManager.LayoutParams;
49 
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 
53 import java.lang.reflect.InvocationTargetException;
54 import java.lang.reflect.Method;
55 import java.util.ArrayList;
56 import java.util.List;
57 
58 /**
59  * Implementation of {@link IWallpaperConnection} that handles communication with a
60  * {@link android.service.wallpaper.WallpaperService}
61  */
62 public class WallpaperConnection extends IWallpaperConnection.Stub implements ServiceConnection {
63 
64     /**
65      * Defines different possible scenarios for which we need to dispatch a command from picker to
66      * the wallpaper.
67      */
68     public enum WhichPreview {
69         /**
70          * Represents the case when we preview a currently applied wallpaper (home/lock) simply
71          * by tapping on it.
72          */
73         PREVIEW_CURRENT(0),
74         /**
75          * Represents the case when we are editing the currently applied wallpaper.
76          */
77         EDIT_CURRENT(1),
78         /**
79          * Represents the case when we are editing a wallpaper that's not currently applied.
80          */
81         EDIT_NON_CURRENT(2);
82 
83         private final int mValue;
84 
WhichPreview(int value)85         WhichPreview(int value) {
86             this.mValue = value;
87         }
88 
getValue()89         public int getValue() {
90             return mValue;
91         }
92     }
93 
94     /**
95      * Returns whether live preview is available in framework.
96      */
isPreviewAvailable()97     public static boolean isPreviewAvailable() {
98         try {
99             return IWallpaperEngine.class.getMethod("mirrorSurfaceControl") != null;
100         } catch (NoSuchMethodException | SecurityException e) {
101             return false;
102         }
103     }
104 
105     private static final String TAG = "WallpaperConnection";
106     private static final Looper sMainLooper = Looper.getMainLooper();
107     private final Context mContext;
108     private final Intent mIntent;
109     private final List<SurfaceControl> mMirrorSurfaceControls = new ArrayList<>();
110     private WallpaperConnectionListener mListener;
111     private SurfaceView mContainerView;
112     private SurfaceView mSecondContainerView;
113     private IWallpaperService mService;
114     @Nullable private IWallpaperEngine mEngine;
115     @Nullable private Point mDisplayMetrics;
116     private boolean mConnected;
117     private boolean mIsVisible;
118     private boolean mIsEngineVisible;
119     private boolean mEngineReady;
120     private boolean mDestroyed;
121     private int mDestinationFlag;
122     private WhichPreview mWhichPreview;
123 
124     /**
125      * @param intent used to bind the wallpaper service
126      * @param context Context used to start and bind the live wallpaper service
127      * @param listener if provided, it'll be notified of connection/disconnection events
128      * @param containerView SurfaceView that will display the wallpaper
129      */
WallpaperConnection(Intent intent, Context context, @Nullable WallpaperConnectionListener listener, @NonNull SurfaceView containerView, WhichPreview preview)130     public WallpaperConnection(Intent intent, Context context,
131             @Nullable WallpaperConnectionListener listener, @NonNull SurfaceView containerView,
132             WhichPreview preview) {
133         this(intent, context, listener, containerView, null, null,
134                 preview);
135     }
136 
137     /**
138      * @param intent used to bind the wallpaper service
139      * @param context Context used to start and bind the live wallpaper service
140      * @param listener if provided, it'll be notified of connection/disconnection events
141      * @param containerView SurfaceView that will display the wallpaper
142      * @param secondaryContainerView optional SurfaceView that will display a second, mirrored
143      *                               version of the wallpaper
144      * @param destinationFlag one of WallpaperManager.FLAG_SYSTEM, WallpaperManager.FLAG_LOCK
145      *                        indicating for which screen we're previewing the wallpaper, or null if
146      *                        unknown
147      */
WallpaperConnection(Intent intent, Context context, @Nullable WallpaperConnectionListener listener, @NonNull SurfaceView containerView, @Nullable SurfaceView secondaryContainerView, @Nullable @WallpaperManager.SetWallpaperFlags Integer destinationFlag, WhichPreview preview)148     public WallpaperConnection(Intent intent, Context context,
149             @Nullable WallpaperConnectionListener listener, @NonNull SurfaceView containerView,
150             @Nullable SurfaceView secondaryContainerView,
151             @Nullable @WallpaperManager.SetWallpaperFlags Integer destinationFlag,
152             WhichPreview preview) {
153         mContext = context.getApplicationContext();
154         mIntent = intent;
155         mListener = listener;
156         mContainerView = containerView;
157         mSecondContainerView = secondaryContainerView;
158         mDestinationFlag = destinationFlag == null ? WallpaperManager.FLAG_SYSTEM : destinationFlag;
159         mWhichPreview = preview;
160     }
161 
162     /**
163      * Bind the Service for this connection.
164      */
connect()165     public boolean connect() {
166         if (mDestroyed) {
167             throw new IllegalStateException("Cannot connect on a destroyed WallpaperConnection");
168         }
169         synchronized (this) {
170             if (mConnected) {
171                 return true;
172             }
173             if (!mContext.bindService(mIntent, this,
174                     Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT
175                             | Context.BIND_ALLOW_ACTIVITY_STARTS)) {
176                 return false;
177             }
178 
179             mConnected = true;
180         }
181 
182         if (mListener != null) {
183             mListener.onConnected();
184         }
185 
186         return true;
187     }
188 
189     /**
190      * Disconnect and destroy the WallpaperEngine for this connection.
191      */
disconnect()192     public void disconnect() {
193         synchronized (this) {
194             mConnected = false;
195             if (mEngine != null) {
196                 try {
197                     mEngine.destroy();
198                     for (SurfaceControl control : mMirrorSurfaceControls) {
199                         control.release();
200                     }
201                     mMirrorSurfaceControls.clear();
202                 } catch (RemoteException e) {
203                     // Ignore
204                 }
205                 mEngine = null;
206             }
207             try {
208                 mContext.unbindService(this);
209             } catch (IllegalArgumentException e) {
210                 Log.i(TAG, "Can't unbind wallpaper service. "
211                         + "It might have crashed, just ignoring.");
212             }
213             mService = null;
214         }
215         if (mListener != null) {
216             mListener.onDisconnected();
217         }
218     }
219 
220     /**
221      * Clean up references on this WallpaperConnection.
222      * After calling this method, {@link #connect()} cannot be called again.
223      */
destroy()224     public void destroy() {
225         disconnect();
226         mContainerView = null;
227         mSecondContainerView = null;
228         mListener = null;
229         mDestroyed = true;
230     }
231 
232     /**
233      * @see ServiceConnection#onServiceConnected(ComponentName, IBinder)
234      */
onServiceConnected(ComponentName name, IBinder service)235     public void onServiceConnected(ComponentName name, IBinder service) {
236         if (mContainerView == null) {
237             return;
238         }
239         mService = IWallpaperService.Stub.asInterface(service);
240         if (mContainerView.getDisplay() == null) {
241             mContainerView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
242                 @Override
243                 public void onViewAttachedToWindow(View v) {
244                     attachConnection(v.getDisplay().getDisplayId());
245                     mContainerView.removeOnAttachStateChangeListener(this);
246                 }
247 
248                 @Override
249                 public void onViewDetachedFromWindow(View v) {}
250             });
251         } else {
252             attachConnection(mContainerView.getDisplay().getDisplayId());
253         }
254     }
255 
256     @Override
onLocalWallpaperColorsChanged(RectF area, WallpaperColors colors, int displayId)257     public void onLocalWallpaperColorsChanged(RectF area,
258             WallpaperColors colors, int displayId) {
259 
260     }
261 
262     /**
263      * @see ServiceConnection#onServiceDisconnected(ComponentName)
264      */
onServiceDisconnected(ComponentName name)265     public void onServiceDisconnected(ComponentName name) {
266         mService = null;
267         mEngine = null;
268         Log.w(TAG, "Wallpaper service gone: " + name);
269     }
270 
271     /**
272      * @see IWallpaperConnection#attachEngine(IWallpaperEngine, int)
273      */
attachEngine(IWallpaperEngine engine, int displayId)274     public void attachEngine(IWallpaperEngine engine, int displayId) {
275         synchronized (this) {
276             if (mConnected) {
277                 mEngine = engine;
278                 if (mIsVisible) {
279                     setEngineVisibility(true);
280                 }
281 
282                 try {
283                     Point displayMetrics = getDisplayMetrics();
284                     // Reset the live wallpaper preview with the correct screen dimensions. It is
285                     // a known issue that the wallpaper service maybe get the Activity window size
286                     // which may differ from the actual physical device screen size, e.g. when in
287                     // 2-pane mode.
288                     // TODO b/262750854 Fix wallpaper service to get the actual physical device
289                     //      screen size instead of the window size that might be smaller when in
290                     //      2-pane mode.
291                     mEngine.resizePreview(new Rect(0, 0, displayMetrics.x, displayMetrics.y));
292                     // Some wallpapers don't trigger #onWallpaperColorsChanged from remote.
293                     // Requesting wallpaper color here to ensure the #onWallpaperColorsChanged
294                     // would get called.
295                     mEngine.requestWallpaperColors();
296                 } catch (RemoteException | NullPointerException e) {
297                     Log.w(TAG, "Failed calling WallpaperEngine APIs", e);
298                 }
299             } else {
300                 try {
301                     engine.destroy();
302                 } catch (RemoteException e) {
303                     // Ignore
304                 }
305             }
306         }
307     }
308 
309     /**
310      * Returns the engine handled by this WallpaperConnection
311      */
312     @Nullable
getEngine()313     public IWallpaperEngine getEngine() {
314         return mEngine;
315     }
316 
317     /**
318      * @see IWallpaperConnection#setWallpaper(String)
319      */
setWallpaper(String name)320     public ParcelFileDescriptor setWallpaper(String name) {
321         return null;
322     }
323 
324     @Override
onWallpaperColorsChanged(WallpaperColors colors, int displayId)325     public void onWallpaperColorsChanged(WallpaperColors colors, int displayId) {
326         if (mContainerView != null) {
327             mContainerView.post(() -> {
328                 if (mListener != null) {
329                     mListener.onWallpaperColorsChanged(colors, displayId);
330                 }
331             });
332         }
333     }
334 
335     @Override
engineShown(IWallpaperEngine engine)336     public void engineShown(IWallpaperEngine engine) {
337         mEngineReady = true;
338         Bundle bundle = new Bundle();
339         bundle.putInt("which_preview", mWhichPreview.getValue());
340         try {
341             engine.dispatchWallpaperCommand("android.wallpaper.previewinfo", 0, 0, 0, bundle);
342         } catch (RemoteException e) {
343             Log.e(TAG, "Error dispatching wallpaper command: " + mWhichPreview.toString());
344         }
345         if (mContainerView != null) {
346             mContainerView.post(() -> reparentWallpaperSurface(mContainerView));
347         }
348         if (mSecondContainerView != null) {
349             mSecondContainerView.post(() -> reparentWallpaperSurface(mSecondContainerView));
350         }
351         if (mContainerView != null) {
352             mContainerView.post(() -> {
353                 if (mListener != null) {
354                     mListener.onEngineShown();
355                 }
356             });
357         }
358     }
359 
360     /**
361      * Returns true if the wallpaper engine has been initialized.
362      */
isEngineReady()363     public boolean isEngineReady() {
364         return mEngineReady;
365     }
366 
367     /**
368      * Sets the engine's visibility.
369      */
setVisibility(boolean visible)370     public void setVisibility(boolean visible) {
371         synchronized (this) {
372             mIsVisible = visible;
373             setEngineVisibility(visible);
374         }
375     }
376 
377 
378     /**
379      * Set the {@link android.app.WallpaperManager.SetWallpaperFlags} to the Engine to indicate
380      * which screen it's being applied/previewed to.
381      */
setWallpaperFlags(@allpaperManager.SetWallpaperFlags int wallpaperFlags)382     public void setWallpaperFlags(@WallpaperManager.SetWallpaperFlags int wallpaperFlags)
383             throws RemoteException {
384         if (mEngine != null && mEngineReady) {
385             mEngine.setWallpaperFlags(wallpaperFlags);
386         }
387     }
388 
attachConnection(int displayId)389     private void attachConnection(int displayId) {
390         try {
391             try {
392                 Method preUMethod = mService.getClass().getMethod("attach",
393                         IWallpaperConnection.class, IBinder.class, int.class, boolean.class,
394                         int.class, int.class, Rect.class, int.class);
395                 preUMethod.invoke(mService, this, mContainerView.getWindowToken(),
396                         LayoutParams.TYPE_APPLICATION_MEDIA, true, mContainerView.getWidth(),
397                         mContainerView.getHeight(), new Rect(0, 0, 0, 0), displayId);
398             } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
399                 Log.d(TAG, "IWallpaperService#attach method without which argument not available, "
400                         + "will use newer version");
401                 // Let's try the new attach method that takes "which" argument
402                 mService.attach(this, mContainerView.getWindowToken(),
403                         LayoutParams.TYPE_APPLICATION_MEDIA, true, mContainerView.getWidth(),
404                         mContainerView.getHeight(), new Rect(0, 0, 0, 0), displayId,
405                         mDestinationFlag, null);
406             }
407         } catch (RemoteException e) {
408             Log.w(TAG, "Failed attaching wallpaper; clearing", e);
409         }
410     }
411 
setEngineVisibility(boolean visible)412     private void setEngineVisibility(boolean visible) {
413         if (mEngine != null && visible != mIsEngineVisible) {
414             try {
415                 mEngine.setVisibility(visible);
416                 mIsEngineVisible = visible;
417             } catch (RemoteException e) {
418                 Log.w(TAG, "Failure setting wallpaper visibility ", e);
419             }
420         }
421     }
422 
reparentWallpaperSurface(SurfaceView parentSurface)423     private void reparentWallpaperSurface(SurfaceView parentSurface) {
424         if (parentSurface == null) {
425             return;
426         }
427         synchronized (this) {
428             if (mEngine == null) {
429                 Log.i(TAG, "Engine is null, was the service disconnected?");
430                 return;
431             }
432         }
433         if (parentSurface.getSurfaceControl() != null) {
434             mirrorAndReparent(parentSurface);
435         } else {
436             Log.d(TAG, "SurfaceView not initialized yet, adding callback");
437             parentSurface.getHolder().addCallback(new Callback() {
438                 @Override
439                 public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
440 
441                 }
442 
443                 @Override
444                 public void surfaceCreated(SurfaceHolder surfaceHolder) {
445                     mirrorAndReparent(parentSurface);
446                     parentSurface.getHolder().removeCallback(this);
447                 }
448 
449                 @Override
450                 public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
451 
452                 }
453             });
454         }
455     }
456 
mirrorAndReparent(SurfaceView parentSurface)457     private void mirrorAndReparent(SurfaceView parentSurface) {
458         IWallpaperEngine engine;
459         synchronized (this) {
460             if (mEngine == null) {
461                 Log.i(TAG, "Engine is null, was the service disconnected?");
462                 return;
463             }
464             engine = mEngine;
465         }
466         try {
467             SurfaceControl parentSC = parentSurface.getSurfaceControl();
468             SurfaceControl wallpaperMirrorSC = engine.mirrorSurfaceControl();
469             if (wallpaperMirrorSC == null) {
470                 return;
471             }
472             float[] values = getScale(parentSurface);
473             try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) {
474                 t.setMatrix(wallpaperMirrorSC, values[MSCALE_X], values[MSKEW_Y],
475                         values[MSKEW_X], values[MSCALE_Y]);
476                 t.reparent(wallpaperMirrorSC, parentSC);
477                 t.show(wallpaperMirrorSC);
478                 t.apply();
479             }
480             synchronized (this) {
481                 mMirrorSurfaceControls.add(wallpaperMirrorSC);
482             }
483         } catch (RemoteException | NullPointerException e) {
484             Log.e(TAG, "Couldn't reparent wallpaper surface", e);
485         }
486     }
487 
getScale(SurfaceView parentSurface)488     private float[] getScale(SurfaceView parentSurface) {
489         Matrix m = new Matrix();
490         float[] values = new float[9];
491         Rect surfacePosition = parentSurface.getHolder().getSurfaceFrame();
492         Point displayMetrics = getDisplayMetrics();
493         m.postScale(((float) surfacePosition.width()) / displayMetrics.x,
494                 ((float) surfacePosition.height()) / displayMetrics.y);
495         m.getValues(values);
496         return values;
497     }
498 
499     /**
500      * Get display metrics. Only call this when the display is attached to the window.
501      */
getDisplayMetrics()502     private Point getDisplayMetrics() {
503         if (mDisplayMetrics != null) {
504             return mDisplayMetrics;
505         }
506         ScreenSizeCalculator screenSizeCalculator = ScreenSizeCalculator.getInstance();
507         Display display = mContainerView.getDisplay();
508         if (display == null) {
509             throw new NullPointerException(
510                     "Display is null due to the view not currently attached to a window.");
511         }
512         mDisplayMetrics = screenSizeCalculator.getScreenSize(display);
513         return mDisplayMetrics;
514     }
515 
516     /**
517      * Interface to be notified of connect/disconnect events from {@link WallpaperConnection}
518      */
519     public interface WallpaperConnectionListener {
520         /**
521          * Called after the Wallpaper service has been bound.
522          */
onConnected()523         default void onConnected() {}
524 
525         /**
526          * Called after the Wallpaper engine has been terminated and the service has been unbound.
527          */
onDisconnected()528         default void onDisconnected() {}
529 
530         /**
531          * Called after the wallpaper has been rendered for the first time.
532          */
onEngineShown()533         default void onEngineShown() {}
534 
535         /**
536          * Called after the wallpaper color is available or updated.
537          */
onWallpaperColorsChanged(WallpaperColors colors, int displayId)538         default void onWallpaperColorsChanged(WallpaperColors colors, int displayId) {}
539     }
540 }
541