1 /* 2 * Copyright (C) 2015 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.stackdivider; 18 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; 20 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 21 import static android.view.Display.DEFAULT_DISPLAY; 22 23 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; 24 25 import android.app.ActivityManager; 26 import android.app.ActivityTaskManager; 27 import android.content.Context; 28 import android.content.res.Configuration; 29 import android.graphics.Rect; 30 import android.os.Handler; 31 import android.provider.Settings; 32 import android.util.Slog; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.window.WindowContainerToken; 36 import android.window.WindowContainerTransaction; 37 import android.window.WindowOrganizer; 38 39 import com.android.internal.policy.DividerSnapAlgorithm; 40 import com.android.systemui.R; 41 import com.android.systemui.SystemUI; 42 import com.android.systemui.TransactionPool; 43 import com.android.systemui.recents.Recents; 44 import com.android.systemui.shared.system.ActivityManagerWrapper; 45 import com.android.systemui.shared.system.TaskStackChangeListener; 46 import com.android.systemui.statusbar.policy.KeyguardStateController; 47 import com.android.systemui.wm.DisplayChangeController; 48 import com.android.systemui.wm.DisplayController; 49 import com.android.systemui.wm.DisplayImeController; 50 import com.android.systemui.wm.DisplayLayout; 51 import com.android.systemui.wm.SystemWindows; 52 53 import java.io.FileDescriptor; 54 import java.io.PrintWriter; 55 import java.lang.ref.WeakReference; 56 import java.util.ArrayList; 57 import java.util.Optional; 58 import java.util.function.Consumer; 59 60 import javax.inject.Singleton; 61 62 import dagger.Lazy; 63 64 /** 65 * Controls the docked stack divider. 66 */ 67 @Singleton 68 public class Divider extends SystemUI implements DividerView.DividerCallbacks, 69 DisplayController.OnDisplaysChangedListener { 70 private static final String TAG = "Divider"; 71 72 static final boolean DEBUG = false; 73 74 static final int DEFAULT_APP_TRANSITION_DURATION = 336; 75 76 private final Optional<Lazy<Recents>> mRecentsOptionalLazy; 77 78 private DividerWindowManager mWindowManager; 79 private DividerView mView; 80 private final DividerState mDividerState = new DividerState(); 81 private boolean mVisible = false; 82 private boolean mMinimized = false; 83 private boolean mAdjustedForIme = false; 84 private boolean mHomeStackResizable = false; 85 private ForcedResizableInfoActivityController mForcedResizableController; 86 private SystemWindows mSystemWindows; 87 private DisplayController mDisplayController; 88 private DisplayImeController mImeController; 89 final TransactionPool mTransactionPool; 90 91 // Keeps track of real-time split geometry including snap positions and ime adjustments 92 private SplitDisplayLayout mSplitLayout; 93 94 // Transient: this contains the layout calculated for a new rotation requested by WM. This is 95 // kept around so that we can wait for a matching configuration change and then use the exact 96 // layout that we sent back to WM. 97 private SplitDisplayLayout mRotateSplitLayout; 98 99 private Handler mHandler; 100 private KeyguardStateController mKeyguardStateController; 101 102 private WindowManagerProxy mWindowManagerProxy; 103 104 private final ArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners = 105 new ArrayList<>(); 106 107 private SplitScreenTaskOrganizer mSplits = new SplitScreenTaskOrganizer(this); 108 109 private DisplayChangeController.OnDisplayChangingListener mRotationController = 110 (display, fromRotation, toRotation, wct) -> { 111 if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) { 112 return; 113 } 114 WindowContainerTransaction t = new WindowContainerTransaction(); 115 DisplayLayout displayLayout = 116 new DisplayLayout(mDisplayController.getDisplayLayout(display)); 117 SplitDisplayLayout sdl = new SplitDisplayLayout(mContext, displayLayout, mSplits); 118 sdl.rotateTo(toRotation); 119 mRotateSplitLayout = sdl; 120 final int position = isDividerVisible() 121 ? (mMinimized ? mView.mSnapTargetBeforeMinimized.position 122 : mView.getCurrentPosition()) 123 // snap resets to middle target when not in split-mode 124 : sdl.getSnapAlgorithm().getMiddleTarget().position; 125 DividerSnapAlgorithm snap = sdl.getSnapAlgorithm(); 126 final DividerSnapAlgorithm.SnapTarget target = 127 snap.calculateNonDismissingSnapTarget(position); 128 sdl.resizeSplits(target.position, t); 129 130 if (isSplitActive() && mHomeStackResizable) { 131 WindowManagerProxy.applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t); 132 } 133 if (mWindowManagerProxy.queueSyncTransactionIfWaiting(t)) { 134 // Because sync transactions are serialized, its possible for an "older" 135 // bounds-change to get applied after a screen rotation. In that case, we 136 // want to actually defer on that rather than apply immediately. Of course, 137 // this means that the bounds may not change until after the rotation so 138 // the user might see some artifacts. This should be rare. 139 Slog.w(TAG, "Screen rotated while other operations were pending, this may" 140 + " result in some graphical artifacts."); 141 } else { 142 wct.merge(t, true /* transfer */); 143 } 144 }; 145 146 private final DividerImeController mImePositionProcessor; 147 148 private TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() { 149 @Override 150 public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, 151 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { 152 if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode() 153 != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY || !mSplits.isSplitScreenSupported()) { 154 return; 155 } 156 157 if (isMinimized()) { 158 onUndockingTask(); 159 } 160 } 161 }; 162 Divider(Context context, Optional<Lazy<Recents>> recentsOptionalLazy, DisplayController displayController, SystemWindows systemWindows, DisplayImeController imeController, Handler handler, KeyguardStateController keyguardStateController, TransactionPool transactionPool)163 public Divider(Context context, Optional<Lazy<Recents>> recentsOptionalLazy, 164 DisplayController displayController, SystemWindows systemWindows, 165 DisplayImeController imeController, Handler handler, 166 KeyguardStateController keyguardStateController, TransactionPool transactionPool) { 167 super(context); 168 mDisplayController = displayController; 169 mSystemWindows = systemWindows; 170 mImeController = imeController; 171 mHandler = handler; 172 mKeyguardStateController = keyguardStateController; 173 mRecentsOptionalLazy = recentsOptionalLazy; 174 mForcedResizableController = new ForcedResizableInfoActivityController(context, this); 175 mTransactionPool = transactionPool; 176 mWindowManagerProxy = new WindowManagerProxy(mTransactionPool, mHandler); 177 mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mHandler); 178 } 179 180 @Override start()181 public void start() { 182 mWindowManager = new DividerWindowManager(mSystemWindows); 183 mDisplayController.addDisplayWindowListener(this); 184 // Hide the divider when keyguard is showing. Even though keyguard/statusbar is above 185 // everything, it is actually transparent except for notifications, so we still need to 186 // hide any surfaces that are below it. 187 // TODO(b/148906453): Figure out keyguard dismiss animation for divider view. 188 mKeyguardStateController.addCallback(new KeyguardStateController.Callback() { 189 @Override 190 public void onUnlockedChanged() { 191 192 } 193 194 @Override 195 public void onKeyguardShowingChanged() { 196 if (!isSplitActive() || mView == null) { 197 return; 198 } 199 mView.setHidden(mKeyguardStateController.isShowing()); 200 if (!mKeyguardStateController.isShowing()) { 201 mImePositionProcessor.updateAdjustForIme(); 202 } 203 } 204 205 @Override 206 public void onKeyguardFadingAwayChanged() { 207 208 } 209 }); 210 // Don't initialize the divider or anything until we get the default display. 211 } 212 213 @Override onDisplayAdded(int displayId)214 public void onDisplayAdded(int displayId) { 215 if (displayId != DEFAULT_DISPLAY) { 216 return; 217 } 218 mSplitLayout = new SplitDisplayLayout(mDisplayController.getDisplayContext(displayId), 219 mDisplayController.getDisplayLayout(displayId), mSplits); 220 mImeController.addPositionProcessor(mImePositionProcessor); 221 mDisplayController.addDisplayChangingController(mRotationController); 222 if (!ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)) { 223 removeDivider(); 224 return; 225 } 226 try { 227 mSplits.init(); 228 // Set starting tile bounds based on middle target 229 final WindowContainerTransaction tct = new WindowContainerTransaction(); 230 int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; 231 mSplitLayout.resizeSplits(midPos, tct); 232 WindowOrganizer.applyTransaction(tct); 233 } catch (Exception e) { 234 Slog.e(TAG, "Failed to register docked stack listener", e); 235 removeDivider(); 236 return; 237 } 238 ActivityManagerWrapper.getInstance().registerTaskStackListener(mActivityRestartListener); 239 } 240 241 @Override onDisplayConfigurationChanged(int displayId, Configuration newConfig)242 public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { 243 if (displayId != DEFAULT_DISPLAY || !mSplits.isSplitScreenSupported()) { 244 return; 245 } 246 mSplitLayout = new SplitDisplayLayout(mDisplayController.getDisplayContext(displayId), 247 mDisplayController.getDisplayLayout(displayId), mSplits); 248 if (mRotateSplitLayout == null) { 249 int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; 250 final WindowContainerTransaction tct = new WindowContainerTransaction(); 251 mSplitLayout.resizeSplits(midPos, tct); 252 WindowOrganizer.applyTransaction(tct); 253 } else if (mSplitLayout.mDisplayLayout.rotation() 254 == mRotateSplitLayout.mDisplayLayout.rotation()) { 255 mSplitLayout.mPrimary = new Rect(mRotateSplitLayout.mPrimary); 256 mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary); 257 mRotateSplitLayout = null; 258 } 259 if (isSplitActive()) { 260 update(newConfig); 261 } 262 } 263 getHandler()264 Handler getHandler() { 265 return mHandler; 266 } 267 getView()268 public DividerView getView() { 269 return mView; 270 } 271 isMinimized()272 public boolean isMinimized() { 273 return mMinimized; 274 } 275 isHomeStackResizable()276 public boolean isHomeStackResizable() { 277 return mHomeStackResizable; 278 } 279 280 /** {@code true} if this is visible */ isDividerVisible()281 public boolean isDividerVisible() { 282 return mView != null && mView.getVisibility() == View.VISIBLE; 283 } 284 285 /** 286 * This indicates that at-least one of the splits has content. This differs from 287 * isDividerVisible because the divider is only visible once *everything* is in split mode 288 * while this only cares if some things are (eg. while entering/exiting as well). 289 */ isSplitActive()290 private boolean isSplitActive() { 291 return mSplits.mPrimary != null && mSplits.mSecondary != null 292 && (mSplits.mPrimary.topActivityType != ACTIVITY_TYPE_UNDEFINED 293 || mSplits.mSecondary.topActivityType != ACTIVITY_TYPE_UNDEFINED); 294 } 295 addDivider(Configuration configuration)296 private void addDivider(Configuration configuration) { 297 Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId()); 298 mView = (DividerView) 299 LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null); 300 DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId()); 301 mView.injectDependencies(mWindowManager, mDividerState, this, mSplits, mSplitLayout, 302 mImePositionProcessor, mWindowManagerProxy); 303 mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE); 304 mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, null /* transaction */); 305 final int size = dctx.getResources().getDimensionPixelSize( 306 com.android.internal.R.dimen.docked_stack_divider_thickness); 307 final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE; 308 final int width = landscape ? size : displayLayout.width(); 309 final int height = landscape ? displayLayout.height() : size; 310 mWindowManager.add(mView, width, height, mContext.getDisplayId()); 311 } 312 removeDivider()313 private void removeDivider() { 314 if (mView != null) { 315 mView.onDividerRemoved(); 316 } 317 mWindowManager.remove(); 318 } 319 update(Configuration configuration)320 private void update(Configuration configuration) { 321 final boolean isDividerHidden = mView != null && mKeyguardStateController.isShowing(); 322 323 removeDivider(); 324 addDivider(configuration); 325 326 if (mMinimized) { 327 mView.setMinimizedDockStack(true, mHomeStackResizable, null /* transaction */); 328 updateTouchable(); 329 } 330 mView.setHidden(isDividerHidden); 331 } 332 onTaskVanished()333 void onTaskVanished() { 334 mHandler.post(this::removeDivider); 335 } 336 updateVisibility(final boolean visible)337 private void updateVisibility(final boolean visible) { 338 if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible); 339 if (mVisible != visible) { 340 mVisible = visible; 341 mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); 342 343 if (visible) { 344 mView.enterSplitMode(mHomeStackResizable); 345 // Update state because animations won't finish. 346 mWindowManagerProxy.runInSync( 347 t -> mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, t)); 348 349 } else { 350 mView.exitSplitMode(); 351 mWindowManagerProxy.runInSync( 352 t -> mView.setMinimizedDockStack(false, mHomeStackResizable, t)); 353 } 354 // Notify existence listeners 355 synchronized (mDockedStackExistsListeners) { 356 mDockedStackExistsListeners.removeIf(wf -> { 357 Consumer<Boolean> l = wf.get(); 358 if (l != null) l.accept(visible); 359 return l == null; 360 }); 361 } 362 } 363 } 364 365 /** Switch to minimized state if appropriate */ setMinimized(final boolean minimized)366 public void setMinimized(final boolean minimized) { 367 if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible); 368 mHandler.post(() -> { 369 if (DEBUG) Slog.d(TAG, "run posted ext setMinimized " + minimized + " vis:" + mVisible); 370 if (!mVisible) { 371 return; 372 } 373 setHomeMinimized(minimized, mHomeStackResizable); 374 }); 375 } 376 setHomeMinimized(final boolean minimized, boolean homeStackResizable)377 private void setHomeMinimized(final boolean minimized, boolean homeStackResizable) { 378 if (DEBUG) { 379 Slog.d(TAG, "setHomeMinimized min:" + mMinimized + "->" + minimized + " hrsz:" 380 + mHomeStackResizable + "->" + homeStackResizable 381 + " split:" + isDividerVisible()); 382 } 383 WindowContainerTransaction wct = new WindowContainerTransaction(); 384 final boolean minimizedChanged = mMinimized != minimized; 385 // Update minimized state 386 if (minimizedChanged) { 387 mMinimized = minimized; 388 } 389 // Always set this because we could be entering split when mMinimized is already true 390 wct.setFocusable(mSplits.mPrimary.token, !mMinimized); 391 boolean onlyFocusable = true; 392 393 // Update home-stack resizability 394 final boolean homeResizableChanged = mHomeStackResizable != homeStackResizable; 395 if (homeResizableChanged) { 396 mHomeStackResizable = homeStackResizable; 397 if (isDividerVisible()) { 398 WindowManagerProxy.applyHomeTasksMinimized( 399 mSplitLayout, mSplits.mSecondary.token, wct); 400 onlyFocusable = false; 401 } 402 } 403 404 // Sync state to DividerView if it exists. 405 if (mView != null) { 406 final int displayId = mView.getDisplay() != null 407 ? mView.getDisplay().getDisplayId() : DEFAULT_DISPLAY; 408 // pause ime here (before updateMinimizedDockedStack) 409 if (mMinimized) { 410 mImePositionProcessor.pause(displayId); 411 } 412 if (minimizedChanged || homeResizableChanged) { 413 // This conflicts with IME adjustment, so only call it when things change. 414 mView.setMinimizedDockStack(minimized, getAnimDuration(), homeStackResizable); 415 } 416 if (!mMinimized) { 417 // afterwards so it can end any animations started in view 418 mImePositionProcessor.resume(displayId); 419 } 420 } 421 updateTouchable(); 422 if (onlyFocusable) { 423 // If we are only setting focusability, a sync transaction isn't necessary (in fact it 424 // can interrupt other animations), so see if it can be submitted on pending instead. 425 if (!mSplits.mDivider.getWmProxy().queueSyncTransactionIfWaiting(wct)) { 426 WindowOrganizer.applyTransaction(wct); 427 } 428 } else { 429 mWindowManagerProxy.applySyncTransaction(wct); 430 } 431 } 432 setAdjustedForIme(boolean adjustedForIme)433 void setAdjustedForIme(boolean adjustedForIme) { 434 if (mAdjustedForIme == adjustedForIme) { 435 return; 436 } 437 mAdjustedForIme = adjustedForIme; 438 updateTouchable(); 439 } 440 updateTouchable()441 private void updateTouchable() { 442 mWindowManager.setTouchable(!mAdjustedForIme); 443 } 444 445 /** 446 * Workaround for b/62528361, at the time recents has drawn, it may happen before a 447 * configuration change to the Divider, and internally, the event will be posted to the 448 * subscriber, or DividerView, which has been removed and prevented from resizing. Instead, 449 * register the event handler here and proxy the event to the current DividerView. 450 */ onRecentsDrawn()451 public void onRecentsDrawn() { 452 if (mView != null) { 453 mView.onRecentsDrawn(); 454 } 455 } 456 onUndockingTask()457 public void onUndockingTask() { 458 if (mView != null) { 459 mView.onUndockingTask(); 460 } 461 } 462 onDockedFirstAnimationFrame()463 public void onDockedFirstAnimationFrame() { 464 if (mView != null) { 465 mView.onDockedFirstAnimationFrame(); 466 } 467 } 468 onDockedTopTask()469 public void onDockedTopTask() { 470 if (mView != null) { 471 mView.onDockedTopTask(); 472 } 473 } 474 onAppTransitionFinished()475 public void onAppTransitionFinished() { 476 if (mView == null) { 477 return; 478 } 479 mForcedResizableController.onAppTransitionFinished(); 480 } 481 482 @Override onDraggingStart()483 public void onDraggingStart() { 484 mForcedResizableController.onDraggingStart(); 485 } 486 487 @Override onDraggingEnd()488 public void onDraggingEnd() { 489 mForcedResizableController.onDraggingEnd(); 490 } 491 492 @Override growRecents()493 public void growRecents() { 494 mRecentsOptionalLazy.ifPresent(recentsLazy -> recentsLazy.get().growRecents()); 495 } 496 497 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)498 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 499 pw.print(" mVisible="); pw.println(mVisible); 500 pw.print(" mMinimized="); pw.println(mMinimized); 501 pw.print(" mAdjustedForIme="); pw.println(mAdjustedForIme); 502 } 503 getAnimDuration()504 long getAnimDuration() { 505 float transitionScale = Settings.Global.getFloat(mContext.getContentResolver(), 506 Settings.Global.TRANSITION_ANIMATION_SCALE, 507 mContext.getResources().getFloat( 508 com.android.internal.R.dimen 509 .config_appTransitionAnimationDurationScaleDefault)); 510 final long transitionDuration = DEFAULT_APP_TRANSITION_DURATION; 511 return (long) (transitionDuration * transitionScale); 512 } 513 514 /** Register a listener that gets called whenever the existence of the divider changes */ registerInSplitScreenListener(Consumer<Boolean> listener)515 public void registerInSplitScreenListener(Consumer<Boolean> listener) { 516 listener.accept(isDividerVisible()); 517 synchronized (mDockedStackExistsListeners) { 518 mDockedStackExistsListeners.add(new WeakReference<>(listener)); 519 } 520 } 521 startEnterSplit()522 void startEnterSplit() { 523 update(mDisplayController.getDisplayContext( 524 mContext.getDisplayId()).getResources().getConfiguration()); 525 // Set resizable directly here because applyEnterSplit already resizes home stack. 526 mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout); 527 } 528 startDismissSplit()529 void startDismissSplit() { 530 mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, true /* dismissOrMaximize */); 531 updateVisibility(false /* visible */); 532 mMinimized = false; 533 removeDivider(); 534 mImePositionProcessor.reset(); 535 } 536 ensureMinimizedSplit()537 void ensureMinimizedSplit() { 538 setHomeMinimized(true /* minimized */, mHomeStackResizable); 539 if (mView != null && !isDividerVisible()) { 540 // Wasn't in split-mode yet, so enter now. 541 if (DEBUG) { 542 Slog.d(TAG, " entering split mode with minimized=true"); 543 } 544 updateVisibility(true /* visible */); 545 } 546 } 547 ensureNormalSplit()548 void ensureNormalSplit() { 549 setHomeMinimized(false /* minimized */, mHomeStackResizable); 550 if (mView != null && !isDividerVisible()) { 551 // Wasn't in split-mode, so enter now. 552 if (DEBUG) { 553 Slog.d(TAG, " enter split mode unminimized "); 554 } 555 updateVisibility(true /* visible */); 556 } 557 } 558 getSplitLayout()559 SplitDisplayLayout getSplitLayout() { 560 return mSplitLayout; 561 } 562 getWmProxy()563 WindowManagerProxy getWmProxy() { 564 return mWindowManagerProxy; 565 } 566 567 /** @return the container token for the secondary split root task. */ getSecondaryRoot()568 public WindowContainerToken getSecondaryRoot() { 569 if (mSplits == null || mSplits.mSecondary == null) { 570 return null; 571 } 572 return mSplits.mSecondary.token; 573 } 574 } 575