1 /* 2 * Copyright (C) 2021 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 17 package com.android.systemui.dreams; 18 19 import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE; 20 import static com.android.systemui.dreams.dagger.DreamModule.DREAM_TOUCH_INSET_MANAGER; 21 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.graphics.drawable.ColorDrawable; 25 import android.util.Log; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.view.Window; 29 import android.view.WindowInsets; 30 import android.view.WindowManager; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.annotation.VisibleForTesting; 35 import androidx.lifecycle.Lifecycle; 36 import androidx.lifecycle.LifecycleRegistry; 37 import androidx.lifecycle.ViewModelStore; 38 39 import com.android.dream.lowlight.dagger.LowLightDreamModule; 40 import com.android.internal.logging.UiEvent; 41 import com.android.internal.logging.UiEventLogger; 42 import com.android.internal.policy.PhoneWindow; 43 import com.android.keyguard.KeyguardUpdateMonitor; 44 import com.android.keyguard.KeyguardUpdateMonitorCallback; 45 import com.android.systemui.complication.Complication; 46 import com.android.systemui.complication.dagger.ComplicationComponent; 47 import com.android.systemui.dagger.qualifiers.Main; 48 import com.android.systemui.dreams.dagger.DreamOverlayComponent; 49 import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor; 50 import com.android.systemui.touch.TouchInsetManager; 51 import com.android.systemui.util.concurrency.DelayableExecutor; 52 53 import java.util.Arrays; 54 import java.util.HashSet; 55 56 import javax.inject.Inject; 57 import javax.inject.Named; 58 59 /** 60 * The {@link DreamOverlayService} is responsible for placing an overlay on top of a dream. The 61 * dream reaches directly out to the service with a Window reference (via LayoutParams), which the 62 * service uses to insert its own child Window into the dream's parent Window. 63 */ 64 public class DreamOverlayService extends android.service.dreams.DreamOverlayService { 65 private static final String TAG = "DreamOverlayService"; 66 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 67 68 // The Context is used to construct the hosting constraint layout and child overlay views. 69 private final Context mContext; 70 // The Executor ensures actions and ui updates happen on the same thread. 71 private final DelayableExecutor mExecutor; 72 // A controller for the dream overlay container view (which contains both the status bar and the 73 // content area). 74 private DreamOverlayContainerViewController mDreamOverlayContainerViewController; 75 private final DreamOverlayCallbackController mDreamOverlayCallbackController; 76 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 77 @Nullable 78 private final ComponentName mLowLightDreamComponent; 79 private final UiEventLogger mUiEventLogger; 80 private final WindowManager mWindowManager; 81 private final String mWindowTitle; 82 83 // A reference to the {@link Window} used to hold the dream overlay. 84 private Window mWindow; 85 86 // True if a dream has bound to the service and dream overlay service has started. 87 private boolean mStarted = false; 88 89 // True if the service has been destroyed. 90 private boolean mDestroyed = false; 91 92 private final ComplicationComponent mComplicationComponent; 93 94 private final com.android.systemui.dreams.complication.dagger.ComplicationComponent 95 mDreamComplicationComponent; 96 97 private final DreamOverlayComponent mDreamOverlayComponent; 98 99 private final DreamOverlayLifecycleOwner mLifecycleOwner; 100 private final LifecycleRegistry mLifecycleRegistry; 101 102 private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor; 103 104 private final KeyguardUpdateMonitorCallback mKeyguardCallback = 105 new KeyguardUpdateMonitorCallback() { 106 @Override 107 public void onShadeExpandedChanged(boolean expanded) { 108 mExecutor.execute(() -> { 109 if (getCurrentStateLocked() != Lifecycle.State.RESUMED 110 && getCurrentStateLocked() != Lifecycle.State.STARTED) { 111 return; 112 } 113 114 setCurrentStateLocked( 115 expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED); 116 }); 117 } 118 }; 119 120 private final DreamOverlayStateController.Callback mExitAnimationFinishedCallback = 121 new DreamOverlayStateController.Callback() { 122 @Override 123 public void onStateChanged() { 124 if (!mStateController.areExitAnimationsRunning()) { 125 mStateController.removeCallback(mExitAnimationFinishedCallback); 126 resetCurrentDreamOverlayLocked(); 127 } 128 } 129 }; 130 131 private final DreamOverlayStateController mStateController; 132 133 @VisibleForTesting 134 public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum { 135 @UiEvent(doc = "The dream overlay has entered start.") 136 DREAM_OVERLAY_ENTER_START(989), 137 @UiEvent(doc = "The dream overlay has completed start.") 138 DREAM_OVERLAY_COMPLETE_START(990); 139 140 private final int mId; 141 DreamOverlayEvent(int id)142 DreamOverlayEvent(int id) { 143 mId = id; 144 } 145 146 @Override getId()147 public int getId() { 148 return mId; 149 } 150 } 151 152 @Inject DreamOverlayService( Context context, DreamOverlayLifecycleOwner lifecycleOwner, @Main DelayableExecutor executor, WindowManager windowManager, ComplicationComponent.Factory complicationComponentFactory, com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory dreamComplicationComponentFactory, DreamOverlayComponent.Factory dreamOverlayComponentFactory, DreamOverlayStateController stateController, KeyguardUpdateMonitor keyguardUpdateMonitor, UiEventLogger uiEventLogger, @Named(DREAM_TOUCH_INSET_MANAGER) TouchInsetManager touchInsetManager, @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) ComponentName lowLightDreamComponent, DreamOverlayCallbackController dreamOverlayCallbackController, @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle)153 public DreamOverlayService( 154 Context context, 155 DreamOverlayLifecycleOwner lifecycleOwner, 156 @Main DelayableExecutor executor, 157 WindowManager windowManager, 158 ComplicationComponent.Factory complicationComponentFactory, 159 com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory 160 dreamComplicationComponentFactory, 161 DreamOverlayComponent.Factory dreamOverlayComponentFactory, 162 DreamOverlayStateController stateController, 163 KeyguardUpdateMonitor keyguardUpdateMonitor, 164 UiEventLogger uiEventLogger, 165 @Named(DREAM_TOUCH_INSET_MANAGER) TouchInsetManager touchInsetManager, 166 @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) 167 ComponentName lowLightDreamComponent, 168 DreamOverlayCallbackController dreamOverlayCallbackController, 169 @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) { 170 super(executor); 171 mContext = context; 172 mExecutor = executor; 173 mWindowManager = windowManager; 174 mKeyguardUpdateMonitor = keyguardUpdateMonitor; 175 mLowLightDreamComponent = lowLightDreamComponent; 176 mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback); 177 mStateController = stateController; 178 mUiEventLogger = uiEventLogger; 179 mDreamOverlayCallbackController = dreamOverlayCallbackController; 180 mWindowTitle = windowTitle; 181 182 final ViewModelStore viewModelStore = new ViewModelStore(); 183 final Complication.Host host = 184 () -> mExecutor.execute(DreamOverlayService.this::requestExit); 185 186 mComplicationComponent = complicationComponentFactory.create(lifecycleOwner, host, 187 viewModelStore, touchInsetManager); 188 mDreamComplicationComponent = dreamComplicationComponentFactory.create( 189 mComplicationComponent.getVisibilityController(), touchInsetManager); 190 mDreamOverlayComponent = dreamOverlayComponentFactory.create(lifecycleOwner, 191 mComplicationComponent.getComplicationHostViewController(), touchInsetManager, 192 new HashSet<>(Arrays.asList( 193 mDreamComplicationComponent.getHideComplicationTouchHandler()))); 194 mLifecycleOwner = lifecycleOwner; 195 mLifecycleRegistry = mLifecycleOwner.getRegistry(); 196 197 mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED)); 198 } 199 200 @Override onDestroy()201 public void onDestroy() { 202 mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback); 203 204 mExecutor.execute(() -> { 205 setCurrentStateLocked(Lifecycle.State.DESTROYED); 206 207 resetCurrentDreamOverlayLocked(); 208 209 mDestroyed = true; 210 }); 211 212 super.onDestroy(); 213 } 214 215 @Override onStartDream(@onNull WindowManager.LayoutParams layoutParams)216 public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { 217 setCurrentStateLocked(Lifecycle.State.STARTED); 218 219 mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START); 220 221 if (mDestroyed) { 222 // The task could still be executed after the service has been destroyed. Bail if 223 // that is the case. 224 return; 225 } 226 227 if (mStarted) { 228 // Reset the current dream overlay before starting a new one. This can happen 229 // when two dreams overlap (briefly, for a smoother dream transition) and both 230 // dreams are bound to the dream overlay service. 231 resetCurrentDreamOverlayLocked(); 232 } 233 234 mDreamOverlayContainerViewController = 235 mDreamOverlayComponent.getDreamOverlayContainerViewController(); 236 mDreamOverlayTouchMonitor = mDreamOverlayComponent.getDreamOverlayTouchMonitor(); 237 mDreamOverlayTouchMonitor.init(); 238 239 mStateController.setShouldShowComplications(shouldShowComplications()); 240 241 // If we are not able to add the overlay window, reset the overlay. 242 if (!addOverlayWindowLocked(layoutParams)) { 243 resetCurrentDreamOverlayLocked(); 244 return; 245 } 246 247 setCurrentStateLocked(Lifecycle.State.RESUMED); 248 mStateController.setOverlayActive(true); 249 final ComponentName dreamComponent = getDreamComponent(); 250 mStateController.setLowLightActive( 251 dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent)); 252 mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START); 253 254 mDreamOverlayCallbackController.onStartDream(); 255 mStarted = true; 256 } 257 258 @Override onEndDream()259 public void onEndDream() { 260 resetCurrentDreamOverlayLocked(); 261 } 262 getCurrentStateLocked()263 private Lifecycle.State getCurrentStateLocked() { 264 return mLifecycleRegistry.getCurrentState(); 265 } 266 setCurrentStateLocked(Lifecycle.State state)267 private void setCurrentStateLocked(Lifecycle.State state) { 268 mLifecycleRegistry.setCurrentState(state); 269 } 270 271 @Override onWakeUp()272 public void onWakeUp() { 273 if (mDreamOverlayContainerViewController != null) { 274 mDreamOverlayCallbackController.onWakeUp(); 275 mDreamOverlayContainerViewController.wakeUp(); 276 } 277 } 278 279 /** 280 * Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be 281 * called from the main executing thread. The window attributes closely mirror those that are 282 * set by the {@link android.service.dreams.DreamService} on the dream Window. 283 * 284 * @param layoutParams The {@link android.view.WindowManager.LayoutParams} which allow inserting 285 * into the dream window. 286 */ addOverlayWindowLocked(WindowManager.LayoutParams layoutParams)287 private boolean addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) { 288 mWindow = new PhoneWindow(mContext); 289 // Default to SystemUI name for TalkBack. 290 mWindow.setTitle(mWindowTitle); 291 mWindow.setAttributes(layoutParams); 292 mWindow.setWindowManager(null, layoutParams.token, "DreamOverlay", true); 293 294 mWindow.setBackgroundDrawable(new ColorDrawable(0)); 295 296 mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 297 mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); 298 mWindow.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); 299 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 300 // Hide all insets when the dream is showing 301 mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); 302 mWindow.setDecorFitsSystemWindows(false); 303 304 if (DEBUG) { 305 Log.d(TAG, "adding overlay window to dream"); 306 } 307 308 mDreamOverlayContainerViewController.init(); 309 // Make extra sure the container view has been removed from its old parent (otherwise we 310 // risk an IllegalStateException in some cases when setting the container view as the 311 // window's content view and the container view hasn't been properly removed previously). 312 removeContainerViewFromParentLocked(); 313 314 mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView()); 315 316 // It is possible that a dream's window (and the dream as a whole) is no longer valid by 317 // the time the overlay service processes the dream. This can happen for example if 318 // another dream is started immediately after the existing dream begins. In this case, the 319 // overlay service should identify the situation through the thrown exception and tear down 320 // the overlay. 321 try { 322 mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); 323 return true; 324 } catch (WindowManager.BadTokenException exception) { 325 Log.e(TAG, "Dream activity window invalid: " + layoutParams.packageName, 326 exception); 327 return false; 328 } 329 } 330 removeContainerViewFromParentLocked()331 private void removeContainerViewFromParentLocked() { 332 View containerView = mDreamOverlayContainerViewController.getContainerView(); 333 if (containerView == null) { 334 return; 335 } 336 ViewGroup parentView = (ViewGroup) containerView.getParent(); 337 if (parentView == null) { 338 return; 339 } 340 Log.w(TAG, "Removing dream overlay container view parent!"); 341 parentView.removeView(containerView); 342 } 343 resetCurrentDreamOverlayLocked()344 private void resetCurrentDreamOverlayLocked() { 345 if (mStateController.areExitAnimationsRunning()) { 346 mStateController.addCallback(mExitAnimationFinishedCallback); 347 return; 348 } 349 350 if (mStarted && mWindow != null) { 351 try { 352 mWindowManager.removeView(mWindow.getDecorView()); 353 } catch (IllegalArgumentException e) { 354 Log.e(TAG, "Error removing decor view when resetting overlay", e); 355 } 356 } 357 358 mStateController.setOverlayActive(false); 359 mStateController.setLowLightActive(false); 360 mStateController.setEntryAnimationsFinished(false); 361 362 mDreamOverlayContainerViewController = null; 363 mDreamOverlayTouchMonitor = null; 364 365 mWindow = null; 366 mStarted = false; 367 } 368 } 369