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