Use a SLEEP transition to trigger failsafe animation cancelling
Some remote animations can misbehave. In particular, recents. When this happens in the form of the remote not reporting finish, the transition system can get stuck waiting. Legacy recents had the same problem and solved it by cancelling the recents and independently finishing/cleaning-up after a "failsafe" duration from turning the screen off. This CL sets-up something similar for shell-transitions. It uses SLEEP as a signal to "quickly end all animations". This signal gets sent (via merge) to all the animations. Each animation gets a small amount of time to finish after-which we forcibly finish it. Bug: 267738124 Test: Observe sleep transitions and cancelling in logs. Change-Id: I623fb5a47a5593e23fbc8f2298eff1c44ae3a0fc
This commit is contained in:
@@ -453,6 +453,11 @@ public interface WindowManager extends ViewManager {
|
||||
* @hide
|
||||
*/
|
||||
int TRANSIT_WAKE = 11;
|
||||
/**
|
||||
* The screen is turning off. This is used as a message to stop all animations.
|
||||
* @hide
|
||||
*/
|
||||
int TRANSIT_SLEEP = 12;
|
||||
/**
|
||||
* The first slot for custom transition types. Callers (like Shell) can make use of custom
|
||||
* transition types for dealing with special cases. These types are effectively ignored by
|
||||
@@ -462,7 +467,7 @@ public interface WindowManager extends ViewManager {
|
||||
* implementation.
|
||||
* @hide
|
||||
*/
|
||||
int TRANSIT_FIRST_CUSTOM = 12;
|
||||
int TRANSIT_FIRST_CUSTOM = 13;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
@@ -480,6 +485,7 @@ public interface WindowManager extends ViewManager {
|
||||
TRANSIT_KEYGUARD_UNOCCLUDE,
|
||||
TRANSIT_PIP,
|
||||
TRANSIT_WAKE,
|
||||
TRANSIT_SLEEP,
|
||||
TRANSIT_FIRST_CUSTOM
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@@ -1437,6 +1443,7 @@ public interface WindowManager extends ViewManager {
|
||||
case TRANSIT_KEYGUARD_UNOCCLUDE: return "KEYGUARD_UNOCCLUDE";
|
||||
case TRANSIT_PIP: return "PIP";
|
||||
case TRANSIT_WAKE: return "WAKE";
|
||||
case TRANSIT_SLEEP: return "SLEEP";
|
||||
case TRANSIT_FIRST_CUSTOM: return "FIRST_CUSTOM";
|
||||
default:
|
||||
if (type > TRANSIT_FIRST_CUSTOM) {
|
||||
|
||||
@@ -1297,6 +1297,12 @@
|
||||
"group": "WM_ERROR",
|
||||
"at": "com\/android\/server\/wm\/WindowManagerService.java"
|
||||
},
|
||||
"-894942237": {
|
||||
"message": "Force Playing Transition: %d",
|
||||
"level": "VERBOSE",
|
||||
"group": "WM_DEBUG_WINDOW_TRANSITIONS",
|
||||
"at": "com\/android\/server\/wm\/Transition.java"
|
||||
},
|
||||
"-883738232": {
|
||||
"message": "Adding more than one toast window for UID at a time.",
|
||||
"level": "WARN",
|
||||
@@ -4243,12 +4249,6 @@
|
||||
"group": "WM_DEBUG_ORIENTATION",
|
||||
"at": "com\/android\/server\/wm\/DisplayContent.java"
|
||||
},
|
||||
"1878927091": {
|
||||
"message": "prepareSurface: No changes in animation for %s",
|
||||
"level": "VERBOSE",
|
||||
"group": "WM_DEBUG_ANIM",
|
||||
"at": "com\/android\/server\/wm\/WindowStateAnimator.java"
|
||||
},
|
||||
"1891501279": {
|
||||
"message": "cancelAnimation(): reason=%s",
|
||||
"level": "DEBUG",
|
||||
|
||||
@@ -447,7 +447,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
|
||||
// Already done, so no need to end it.
|
||||
return;
|
||||
}
|
||||
if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
|
||||
if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
|
||||
// queue since no actual animation.
|
||||
} else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
|
||||
if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) {
|
||||
boolean ended = mSplitHandler.end();
|
||||
// If split couldn't end (because it is remote), then don't end everything else
|
||||
@@ -461,8 +463,12 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
|
||||
} else {
|
||||
mPipHandler.end();
|
||||
}
|
||||
} else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
|
||||
// queue
|
||||
} else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
|
||||
mPipHandler.end();
|
||||
if (mixed.mLeftoversHandler != null) {
|
||||
mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
|
||||
finishCallback);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Playing a mixed transition with unknown type? "
|
||||
+ mixed.mType);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.wm.shell.transition;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import android.view.SurfaceControl;
|
||||
import android.window.TransitionInfo;
|
||||
import android.window.TransitionRequestInfo;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* A Simple handler that tracks SLEEP transitions. We track them specially since we (ab)use these
|
||||
* as sentinels for fast-forwarding through animations when the screen is off.
|
||||
*
|
||||
* There should only be one SleepHandler and it is used explicitly by {@link Transitions} so we
|
||||
* don't register it like a normal handler.
|
||||
*/
|
||||
class SleepHandler implements Transitions.TransitionHandler {
|
||||
final ArrayList<IBinder> mSleepTransitions = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
|
||||
@NonNull SurfaceControl.Transaction startTransaction,
|
||||
@NonNull SurfaceControl.Transaction finishTransaction,
|
||||
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
||||
startTransaction.apply();
|
||||
finishCallback.onTransitionFinished(null, null);
|
||||
mSleepTransitions.remove(transition);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
|
||||
@NonNull TransitionRequestInfo request) {
|
||||
mSleepTransitions.add(transition);
|
||||
return new WindowContainerTransaction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
|
||||
@Nullable SurfaceControl.Transaction finishTransaction) {
|
||||
Log.e(Transitions.TAG, "Sleep transition was consumed. This doesn't make sense");
|
||||
mSleepTransitions.remove(transition);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE;
|
||||
import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
|
||||
import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
|
||||
import static android.view.WindowManager.TRANSIT_OPEN;
|
||||
import static android.view.WindowManager.TRANSIT_SLEEP;
|
||||
import static android.view.WindowManager.TRANSIT_TO_BACK;
|
||||
import static android.view.WindowManager.TRANSIT_TO_FRONT;
|
||||
import static android.view.WindowManager.fixScale;
|
||||
@@ -124,6 +125,7 @@ public class Transitions implements RemoteCallable<Transitions> {
|
||||
private final DisplayController mDisplayController;
|
||||
private final ShellController mShellController;
|
||||
private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
|
||||
private final SleepHandler mSleepHandler = new SleepHandler();
|
||||
|
||||
private boolean mIsRegistered = false;
|
||||
|
||||
@@ -137,6 +139,14 @@ public class Transitions implements RemoteCallable<Transitions> {
|
||||
|
||||
private float mTransitionAnimationScaleSetting = 1.0f;
|
||||
|
||||
/**
|
||||
* How much time we allow for an animation to finish itself on sleep. If it takes longer, we
|
||||
* will force-finish it (on this end) which may leave it in a bad state but won't hang the
|
||||
* device. This needs to be pretty small because it is an allowance for each queued animation,
|
||||
* however it can't be too small since there is some potential IPC involved.
|
||||
*/
|
||||
private static final int SLEEP_ALLOWANCE_MS = 120;
|
||||
|
||||
private static final class ActiveTransition {
|
||||
IBinder mToken;
|
||||
TransitionHandler mHandler;
|
||||
@@ -478,11 +488,29 @@ public class Transitions implements RemoteCallable<Transitions> {
|
||||
+ Arrays.toString(mActiveTransitions.stream().map(
|
||||
activeTransition -> activeTransition.mToken).toArray()));
|
||||
}
|
||||
final ActiveTransition active = mActiveTransitions.get(activeIdx);
|
||||
|
||||
for (int i = 0; i < mObservers.size(); ++i) {
|
||||
mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT);
|
||||
}
|
||||
|
||||
if (info.getType() == TRANSIT_SLEEP) {
|
||||
if (activeIdx > 0) {
|
||||
active.mInfo = info;
|
||||
active.mStartT = t;
|
||||
active.mFinishT = finishT;
|
||||
if (!info.getRootLeash().isValid()) {
|
||||
// Shell has some debug settings which makes calling binders with invalid
|
||||
// surfaces crash, so replace it with a "real" one.
|
||||
info.setRootLeash(new SurfaceControl.Builder().setName("Invalid")
|
||||
.setContainerLayer().build(), 0, 0);
|
||||
}
|
||||
// Sleep starts a process of forcing all prior transitions to finish immediately
|
||||
finishForSleep(null /* forceFinish */);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow to notify keyguard un-occluding state to KeyguardService, which can happen while
|
||||
// screen-off, so there might no visibility change involved.
|
||||
if (!info.getRootLeash().isValid() && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) {
|
||||
@@ -527,7 +555,6 @@ public class Transitions implements RemoteCallable<Transitions> {
|
||||
return;
|
||||
}
|
||||
|
||||
final ActiveTransition active = mActiveTransitions.get(activeIdx);
|
||||
active.mInfo = info;
|
||||
active.mStartT = t;
|
||||
active.mFinishT = finishT;
|
||||
@@ -797,23 +824,30 @@ public class Transitions implements RemoteCallable<Transitions> {
|
||||
}
|
||||
final ActiveTransition active = new ActiveTransition();
|
||||
WindowContainerTransaction wct = null;
|
||||
for (int i = mHandlers.size() - 1; i >= 0; --i) {
|
||||
wct = mHandlers.get(i).handleRequest(transitionToken, request);
|
||||
if (wct != null) {
|
||||
active.mHandler = mHandlers.get(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (request.getDisplayChange() != null) {
|
||||
TransitionRequestInfo.DisplayChange change = request.getDisplayChange();
|
||||
if (change.getEndRotation() != change.getStartRotation()) {
|
||||
// Is a rotation, so dispatch to all displayChange listeners
|
||||
if (wct == null) {
|
||||
wct = new WindowContainerTransaction();
|
||||
|
||||
// If we have sleep, we use a special handler and we try to finish everything ASAP.
|
||||
if (request.getType() == TRANSIT_SLEEP) {
|
||||
mSleepHandler.handleRequest(transitionToken, request);
|
||||
active.mHandler = mSleepHandler;
|
||||
} else {
|
||||
for (int i = mHandlers.size() - 1; i >= 0; --i) {
|
||||
wct = mHandlers.get(i).handleRequest(transitionToken, request);
|
||||
if (wct != null) {
|
||||
active.mHandler = mHandlers.get(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (request.getDisplayChange() != null) {
|
||||
TransitionRequestInfo.DisplayChange change = request.getDisplayChange();
|
||||
if (change.getEndRotation() != change.getStartRotation()) {
|
||||
// Is a rotation, so dispatch to all displayChange listeners
|
||||
if (wct == null) {
|
||||
wct = new WindowContainerTransaction();
|
||||
}
|
||||
mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
|
||||
change.getDisplayId(), change.getStartRotation(),
|
||||
change.getEndRotation(), null /* newDisplayAreaInfo */);
|
||||
}
|
||||
mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
|
||||
change.getDisplayId(), change.getStartRotation(), change.getEndRotation(),
|
||||
null /* newDisplayAreaInfo */);
|
||||
}
|
||||
}
|
||||
mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct);
|
||||
@@ -839,6 +873,56 @@ public class Transitions implements RemoteCallable<Transitions> {
|
||||
return active.mToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish running animations (almost) immediately when a SLEEP transition comes in. We use this
|
||||
* as both a way to reduce unnecessary work (animations not visible while screen off) and as a
|
||||
* failsafe to unblock "stuck" animations (in particular remote animations).
|
||||
*
|
||||
* This works by "merging" the sleep transition into the currently-playing transition (even if
|
||||
* its out-of-order) -- turning SLEEP into a signal. If the playing transition doesn't finish
|
||||
* within `SLEEP_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and
|
||||
* send an abort/consumed message).
|
||||
*
|
||||
* This is then repeated until there are no more pending sleep transitions.
|
||||
*
|
||||
* @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge
|
||||
* signal to -- so it will be force-finished if it's still running.
|
||||
*/
|
||||
private void finishForSleep(@Nullable IBinder forceFinish) {
|
||||
if (mActiveTransitions.isEmpty() || mSleepHandler.mSleepTransitions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (forceFinish != null && mActiveTransitions.get(0).mToken == forceFinish) {
|
||||
Log.e(TAG, "Forcing transition to finish due to sleep timeout: "
|
||||
+ mActiveTransitions.get(0).mToken);
|
||||
onFinish(mActiveTransitions.get(0).mToken, null, null, true);
|
||||
}
|
||||
final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction();
|
||||
while (!mActiveTransitions.isEmpty() && !mSleepHandler.mSleepTransitions.isEmpty()) {
|
||||
final ActiveTransition playing = mActiveTransitions.get(0);
|
||||
int sleepIdx = findActiveTransition(mSleepHandler.mSleepTransitions.get(0));
|
||||
if (sleepIdx >= 0) {
|
||||
// Try to signal that we are sleeping by attempting to merge the sleep transition
|
||||
// into the playing one.
|
||||
final ActiveTransition nextSleep = mActiveTransitions.get(sleepIdx);
|
||||
playing.mHandler.mergeAnimation(nextSleep.mToken, nextSleep.mInfo, dummyT,
|
||||
playing.mToken, (wct, cb) -> {});
|
||||
} else {
|
||||
Log.e(TAG, "Couldn't find sleep transition in active list: "
|
||||
+ mSleepHandler.mSleepTransitions.get(0));
|
||||
}
|
||||
// it's possible to complete immediately. If that happens, just repeat the signal
|
||||
// loop until we either finish everything or start playing an animation that isn't
|
||||
// finishing immediately.
|
||||
if (!mActiveTransitions.isEmpty() && mActiveTransitions.get(0) == playing) {
|
||||
// Give it a (very) short amount of time to process it before forcing.
|
||||
mMainExecutor.executeDelayed(
|
||||
() -> finishForSleep(playing.mToken), SLEEP_ALLOWANCE_MS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a callback that must be called after a TransitionHandler finishes playing an
|
||||
* animation.
|
||||
|
||||
@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
|
||||
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
|
||||
import static android.view.WindowManager.TRANSIT_CHANGE;
|
||||
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
|
||||
import static android.view.WindowManager.TRANSIT_SLEEP;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActivityManager;
|
||||
@@ -46,6 +47,7 @@ import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.wm.shell.util.TransitionUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Helper class to build {@link RemoteTransition} objects
|
||||
@@ -205,6 +207,12 @@ public class RemoteTransitionCompat {
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
boolean merge(TransitionInfo info, SurfaceControl.Transaction t) {
|
||||
if (info.getType() == TRANSIT_SLEEP) {
|
||||
// A sleep event means we need to stop animations immediately, so cancel here.
|
||||
mListener.onAnimationCanceled(new HashMap<>());
|
||||
finish(mWillFinishToHome, false /* userLeaveHint */);
|
||||
return false;
|
||||
}
|
||||
ArrayList<TransitionInfo.Change> openingTasks = null;
|
||||
ArrayList<TransitionInfo.Change> closingTasks = null;
|
||||
mAppearedTargets = null;
|
||||
|
||||
@@ -33,6 +33,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
|
||||
import static android.view.WindowManager.TRANSIT_NONE;
|
||||
import static android.view.WindowManager.TRANSIT_PIP;
|
||||
import static android.view.WindowManager.TRANSIT_SLEEP;
|
||||
import static android.view.WindowManager.TRANSIT_TO_BACK;
|
||||
import static android.view.WindowManager.TRANSIT_WAKE;
|
||||
|
||||
@@ -2329,6 +2330,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
|
||||
}
|
||||
|
||||
void applySleepTokens(boolean applyToRootTasks) {
|
||||
boolean builtSleepTransition = false;
|
||||
for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
|
||||
// Set the sleeping state of the display.
|
||||
final DisplayContent display = getChildAt(displayNdx);
|
||||
@@ -2338,6 +2340,30 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
|
||||
}
|
||||
display.setIsSleeping(displayShouldSleep);
|
||||
|
||||
if (display.mTransitionController.isShellTransitionsEnabled() && !builtSleepTransition
|
||||
// Only care if there are actual sleep tokens.
|
||||
&& displayShouldSleep && !display.mAllSleepTokens.isEmpty()) {
|
||||
builtSleepTransition = true;
|
||||
// We don't actually care about collecting anything here. We really just want
|
||||
// this as a signal to the transition-player.
|
||||
final Transition transition = new Transition(TRANSIT_SLEEP, 0 /* flags */,
|
||||
display.mTransitionController, mWmService.mSyncEngine);
|
||||
final Runnable sendSleepTransition = () -> {
|
||||
display.mTransitionController.requestStartTransition(transition,
|
||||
null /* trigger */, null /* remote */, null /* display */);
|
||||
// Force playing immediately so that unrelated ops can't be collected.
|
||||
transition.playNow();
|
||||
};
|
||||
if (display.mTransitionController.isCollecting()) {
|
||||
mWmService.mSyncEngine.queueSyncSet(
|
||||
() -> display.mTransitionController.moveToCollecting(transition),
|
||||
sendSleepTransition);
|
||||
} else {
|
||||
display.mTransitionController.moveToCollecting(transition);
|
||||
sendSleepTransition.run();
|
||||
}
|
||||
}
|
||||
|
||||
if (!applyToRootTasks) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -218,6 +218,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
|
||||
|
||||
final TransitionController.Logger mLogger = new TransitionController.Logger();
|
||||
|
||||
/** Whether this transition was forced to play early (eg for a SLEEP signal). */
|
||||
private boolean mForcePlaying = false;
|
||||
|
||||
/**
|
||||
* {@code false} if this transition runs purely in WMCore (meaning Shell is completely unaware
|
||||
* of it). Currently, this happens before the display is ready since nothing can be seen yet.
|
||||
@@ -1007,6 +1010,25 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
|
||||
mController.dispatchLegacyAppTransitionCancelled();
|
||||
}
|
||||
|
||||
/** Immediately moves this to playing even if it isn't started yet. */
|
||||
void playNow() {
|
||||
if (mState == STATE_PLAYING) return;
|
||||
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Force Playing Transition: %d",
|
||||
mSyncId);
|
||||
mForcePlaying = true;
|
||||
setAllReady();
|
||||
if (mState == STATE_COLLECTING) {
|
||||
start();
|
||||
}
|
||||
// Don't wait for actual surface-placement. We don't want anything else collected in this
|
||||
// transition.
|
||||
mSyncEngine.onSurfacePlacement();
|
||||
}
|
||||
|
||||
boolean isForcePlaying() {
|
||||
return mForcePlaying;
|
||||
}
|
||||
|
||||
void setRemoteTransition(RemoteTransition remoteTransition) {
|
||||
mRemoteTransition = remoteTransition;
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
}
|
||||
transition = mTransitionController.createTransition(type);
|
||||
}
|
||||
if (!transition.isCollecting()) {
|
||||
if (!transition.isCollecting() && !transition.isForcePlaying()) {
|
||||
Slog.e(TAG, "Trying to start a transition that isn't collecting. This probably"
|
||||
+ " means Shell took too long to respond to a request. WM State may be"
|
||||
+ " incorrect now, please file a bug");
|
||||
|
||||
Reference in New Issue
Block a user