Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d8f2268e authored by Jorim Jaggi's avatar Jorim Jaggi Committed by Android (Google) Code Review
Browse files

Merge "Clean up starting window to prepare for saved surfaces"

parents b03bfe0a ba41f4b9
Loading
Loading
Loading
Loading
+18 −9
Original line number Diff line number Diff line
@@ -436,6 +436,15 @@ public interface WindowManagerPolicy {
        void dismiss();
    }

    /**
     * Holds the contents of a starting window. {@link #addSplashScreen} needs to wrap the
     * contents of the starting window into an class implementing this interface, which then will be
     * held by WM and passed into {@link #removeSplashScreen} when the starting window is no
     * longer needed.
     */
    interface StartingSurface {
    }

    /**
     * Interface for calling back in to the window manager that is private
     * between it and the policy.
@@ -738,17 +747,17 @@ public interface WindowManagerPolicy {
     *        context to for resources.
     *
     * @return Optionally you can return the View that was used to create the
     *         window, for easy removal in removeStartingWindow.
     *         window, for easy removal in removeSplashScreen.
     *
     * @see #removeStartingWindow
     * @see #removeSplashScreen
     */
    public View addStartingWindow(IBinder appToken, String packageName,
            int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel,
            int labelRes, int icon, int logo, int windowFlags, Configuration overrideConfig);
    public StartingSurface addSplashScreen(IBinder appToken, String packageName, int theme,
            CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon,
            int logo, int windowFlags, Configuration overrideConfig);

    /**
     * Called when the first window of an application has been displayed, while
     * {@link #addStartingWindow} has created a temporary initial window for
     * {@link #addSplashScreen} has created a temporary initial window for
     * that application.  You should at this point remove the window from the
     * window manager.  This is called without the window manager locked so
     * that you can call back into it.
@@ -759,11 +768,11 @@ public interface WindowManagerPolicy {
     * even if you previously returned one.
     *
     * @param appToken Token of the application that has started.
     * @param window Window View that was returned by createStartingWindow.
     * @param surface Surface that was returned by {@link #addSplashScreen}.
     *
     * @see #addStartingWindow
     * @see #addSplashScreen
     */
    public void removeStartingWindow(IBinder appToken, View window);
    public void removeSplashScreen(IBinder appToken, StartingSurface surface);

    /**
     * Prepare for a window being added to the window manager.  You can throw an
+18 −18
Original line number Diff line number Diff line
@@ -251,9 +251,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
    static final boolean DEBUG_INPUT = false;
    static final boolean DEBUG_KEYGUARD = false;
    static final boolean DEBUG_LAYOUT = false;
    static final boolean DEBUG_STARTING_WINDOW = false;
    static final boolean DEBUG_SPLASH_SCREEN = false;
    static final boolean DEBUG_WAKEUP = false;
    static final boolean SHOW_STARTING_ANIMATIONS = true;
    static final boolean SHOW_SPLASH_SCREENS = true;

    // Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key.
    // No longer recommended for desk docks;
@@ -2794,10 +2794,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {

    /** {@inheritDoc} */
    @Override
    public View addStartingWindow(IBinder appToken, String packageName, int theme,
            CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
            int icon, int logo, int windowFlags, Configuration overrideConfig) {
        if (!SHOW_STARTING_ANIMATIONS) {
    public StartingSurface addSplashScreen(IBinder appToken, String packageName, int theme,
            CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon,
            int logo, int windowFlags, Configuration overrideConfig) {
        if (!SHOW_SPLASH_SCREENS) {
            return null;
        }
        if (packageName == null) {
@@ -2809,7 +2809,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {

        try {
            Context context = mContext;
            if (DEBUG_STARTING_WINDOW) Slog.d(TAG, "addStartingWindow " + packageName
            if (DEBUG_SPLASH_SCREEN) Slog.d(TAG, "addSplashScreen " + packageName
                    + ": nonLocalizedLabel=" + nonLocalizedLabel + " theme="
                    + Integer.toHexString(theme));
            if (theme != context.getThemeResId() || labelRes != 0) {
@@ -2822,8 +2822,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
            }

            if (overrideConfig != null && !overrideConfig.equals(EMPTY)) {
                if (DEBUG_STARTING_WINDOW) Slog.d(TAG, "addStartingWindow: creating context based"
                        + " on overrideConfig" + overrideConfig + " for starting window");
                if (DEBUG_SPLASH_SCREEN) Slog.d(TAG, "addSplashScreen: creating context based"
                        + " on overrideConfig" + overrideConfig + " for splash screen");
                final Context overrideContext = context.createConfigurationContext(overrideConfig);
                overrideContext.setTheme(theme);
                final TypedArray typedArray = overrideContext.obtainStyledAttributes(
@@ -2833,7 +2833,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
                    // We want to use the windowBackground for the override context if it is
                    // available, otherwise we use the default one to make sure a themed starting
                    // window is displayed for the app.
                    if (DEBUG_STARTING_WINDOW) Slog.d(TAG, "addStartingWindow: apply overrideConfig"
                    if (DEBUG_SPLASH_SCREEN) Slog.d(TAG, "addSplashScreen: apply overrideConfig"
                            + overrideConfig + " to starting window resId=" + resId);
                    context = overrideContext;
                }
@@ -2895,19 +2895,19 @@ public class PhoneWindowManager implements WindowManagerPolicy {
                params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
            }

            params.setTitle("Starting " + packageName);
            params.setTitle("Splash Screen " + packageName);

            wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            view = win.getDecorView();

            if (DEBUG_STARTING_WINDOW) Slog.d(TAG, "Adding starting window for "
            if (DEBUG_SPLASH_SCREEN) Slog.d(TAG, "Adding splash screen window for "
                + packageName + " / " + appToken + ": " + (view.getParent() != null ? view : null));

            wm.addView(view, params);

            // Only return the view if it was successfully added to the
            // window manager... which we can tell by it having a parent.
            return view.getParent() != null ? view : null;
            return view.getParent() != null ? new SplashScreenSurface(view) : null;
        } catch (WindowManager.BadTokenException e) {
            // ignore
            Log.w(TAG, appToken + " already running, starting window not displayed. " +
@@ -2929,13 +2929,13 @@ public class PhoneWindowManager implements WindowManagerPolicy {

    /** {@inheritDoc} */
    @Override
    public void removeStartingWindow(IBinder appToken, View window) {
        if (DEBUG_STARTING_WINDOW) Slog.v(TAG, "Removing starting window for " + appToken + ": "
                + window + " Callers=" + Debug.getCallers(4));
    public void removeSplashScreen(IBinder appToken, StartingSurface surface) {
        if (DEBUG_SPLASH_SCREEN) Slog.v(TAG, "Removing splash screen window for " + appToken + ": "
                + surface + " Callers=" + Debug.getCallers(4));

        if (window != null) {
        if (surface != null) {
            WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
            wm.removeView(window);
            wm.removeView(((SplashScreenSurface) surface).view);
        }
    }

+38 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.server.policy;

import android.view.View;
import android.view.WindowManagerPolicy;
import android.view.WindowManagerPolicy.StartingSurface;

import com.android.internal.policy.DecorView;
import com.android.internal.policy.PhoneWindow;

/**
 * Holds the contents of a splash screen starting window, i.e. the {@link DecorView} of a
 * {@link PhoneWindow}. This is just a wrapper such that we can return it from
 * {@link WindowManagerPolicy#addSplashScreen}.
 */
class SplashScreenSurface implements StartingSurface {

    final View view;

    SplashScreenSurface(View view) {
        this.view = view;
    }
}
+128 −16
Original line number Diff line number Diff line
@@ -27,21 +27,21 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WIND
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TOKEN_MOVEMENT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.H.ADD_STARTING;

import android.graphics.Bitmap;
import android.os.Trace;
import com.android.server.AttributeCache;

import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.os.Binder;
import android.os.Debug;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Looper;
import android.os.Trace;
import android.util.Slog;
import android.view.IApplicationToken;
import android.view.WindowManagerPolicy.StartingSurface;

import com.android.server.AttributeCache;
/**
 * Controller for the app window token container. This is created by activity manager to link
 * activity records to the app window token container they use in window manager.
@@ -52,6 +52,7 @@ public class AppWindowContainerController
        extends WindowContainerController<AppWindowToken, AppWindowContainerListener> {

    private final IApplicationToken mToken;
    private final Handler mHandler = new Handler(Looper.getMainLooper());

    private final Runnable mOnWindowsDrawn = () -> {
        if (mListener == null) {
@@ -80,6 +81,94 @@ public class AppWindowContainerController
        mListener.onWindowsGone();
    };

    private final Runnable mAddStartingWindow = () -> {
        final StartingData startingData;
        final Configuration mergedOverrideConfiguration;

        synchronized (mWindowMap) {
            startingData = mContainer.startingData;
            mergedOverrideConfiguration = mContainer.getMergedOverrideConfiguration();
        }

        if (startingData == null) {
            // Animation has been canceled... do nothing.
            return;
        }

        if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Add starting "
                + this + ": pkg=" + mContainer.startingData.pkg);

        StartingSurface contents = null;
        try {
            contents = mService.mPolicy.addSplashScreen(mContainer.token, startingData.pkg,
                    startingData.theme, startingData.compatInfo, startingData.nonLocalizedLabel,
                    startingData.labelRes, startingData.icon, startingData.logo,
                    startingData.windowFlags, mergedOverrideConfiguration);
        } catch (Exception e) {
            Slog.w(TAG_WM, "Exception when adding starting window", e);
        }
        if (contents != null) {
            boolean abort = false;

            synchronized(mWindowMap) {
                if (mContainer.removed || mContainer.startingData == null) {
                    // If the window was successfully added, then
                    // we need to remove it.
                    if (mContainer.startingWindow != null) {
                        if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM,
                                "Aborted starting " + mContainer
                                        + ": removed=" + mContainer.removed
                                        + " startingData=" + mContainer.startingData);
                        mContainer.startingWindow = null;
                        mContainer.startingData = null;
                        abort = true;
                    }
                } else {
                    mContainer.startingSurface = contents;
                }
                if (DEBUG_STARTING_WINDOW && !abort) Slog.v(TAG_WM,
                        "Added starting " + mContainer
                                + ": startingWindow="
                                + mContainer.startingWindow + " startingView="
                                + mContainer.startingSurface);
            }

            if (abort) {
                try {
                    mService.mPolicy.removeSplashScreen(mContainer.token, contents);
                } catch (Exception e) {
                    Slog.w(TAG_WM, "Exception when removing starting window", e);
                }
            }
        }
    };

    private final Runnable mRemoveStartingWindow = () -> {
        IBinder token = null;
        StartingSurface contents = null;
        synchronized (mWindowMap) {
            if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Remove starting "
                    + mContainer + ": startingWindow="
                    + mContainer.startingWindow + " startingView="
                    + mContainer.startingSurface);
            if (mContainer.startingWindow != null) {
                contents = mContainer.startingSurface;
                token = mContainer.token;
                mContainer.startingData = null;
                mContainer.startingSurface = null;
                mContainer.startingWindow = null;
                mContainer.startingDisplayed = false;
            }
        }
        if (contents != null) {
            try {
                mService.mPolicy.removeSplashScreen(token, contents);
            } catch (Exception e) {
                Slog.w(TAG_WM, "Exception when removing starting window", e);
            }
        }
    };

    public AppWindowContainerController(IApplicationToken token,
            AppWindowContainerListener listener, int taskId, int index, int requestedOrientation,
            boolean fullscreen, boolean showForAllUsers, int configChanges,
@@ -393,19 +482,42 @@ public class AppWindowContainerController
            if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Creating StartingData");
            mContainer.startingData = new StartingData(pkg, theme, compatInfo, nonLocalizedLabel,
                    labelRes, icon, logo, windowFlags);
            final Message m = mService.mH.obtainMessage(ADD_STARTING, mContainer);
            scheduleAddStartingWindow();
        }
        return true;
    }

    void scheduleAddStartingWindow() {

        // Note: we really want to do sendMessageAtFrontOfQueue() because we
        // want to process the message ASAP, before any other queued
        // messages.
        if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Enqueueing ADD_STARTING");
            mService.mH.sendMessageAtFrontOfQueue(m);
        }
        return true;
        mHandler.postAtFrontOfQueue(mAddStartingWindow);
    }

    public void removeStartingWindow() {
        synchronized (mWindowMap) {
            mService.scheduleRemoveStartingWindowLocked(mContainer);
            if (mHandler.hasCallbacks(mRemoveStartingWindow)) {
                // Already scheduled.
                return;
            }

            if (mContainer.startingWindow == null) {
                if (mContainer.startingData != null) {
                    // Starting window has not been added yet, but it is scheduled to be added.
                    // Go ahead and cancel the request.
                    if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM,
                            "Clearing startingData for token=" + mContainer);
                    mContainer.startingData = null;
                }
                return;
            }

            if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, Debug.getCallers(1)
                    + ": Schedule remove starting " + mContainer
                    + " startingWindow=" + mContainer.startingWindow);
            mHandler.post(mRemoveStartingWindow);
        }
    }

@@ -508,15 +620,15 @@ public class AppWindowContainerController


    void reportWindowsDrawn() {
        mService.mH.post(mOnWindowsDrawn);
        mHandler.post(mOnWindowsDrawn);
    }

    void reportWindowsVisible() {
        mService.mH.post(mOnWindowsVisible);
        mHandler.post(mOnWindowsVisible);
    }

    void reportWindowsGone() {
        mService.mH.post(mOnWindowsGone);
        mHandler.post(mOnWindowsGone);
    }

    /** Calls directly into activity manager so window manager lock shouldn't held. */
+33 −32
Original line number Diff line number Diff line
@@ -29,9 +29,9 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
import static android.view.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static com.android.server.wm.AppTransition.TRANSIT_UNSET;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_FOCUS_LIGHT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ORIENTATION;
@@ -47,22 +47,21 @@ import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES;
import static com.android.server.wm.WindowManagerService.logWithStack;

import android.os.Debug;
import com.android.internal.util.ToBooleanFunction;
import com.android.server.input.InputApplicationHandle;
import com.android.server.wm.WindowManagerService.H;

import android.annotation.NonNull;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Debug;
import android.os.IBinder;
import android.os.Message;
import android.os.SystemClock;
import android.util.Slog;
import android.view.IApplicationToken;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManagerPolicy.StartingSurface;

import com.android.internal.util.ToBooleanFunction;
import com.android.server.input.InputApplicationHandle;
import com.android.server.wm.WindowManagerService.H;

import java.io.PrintWriter;
import java.util.ArrayDeque;
@@ -138,7 +137,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
    // Information about an application starting window if displayed.
    StartingData startingData;
    WindowState startingWindow;
    View startingView;
    StartingSurface startingSurface;
    boolean startingDisplayed;
    boolean startingMoved;
    boolean firstWindowDrawn;
@@ -213,8 +212,9 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
            // it from behind the starting window, so there is no need for it to also be doing its
            // own stuff.
            winAnimator.clearAnimation();
            winAnimator.mService.mFinishedStarting.add(this);
            winAnimator.mService.mH.sendEmptyMessage(H.FINISHED_STARTING);
            if (getController() != null) {
                getController().removeStartingWindow();
            }
        }
        updateReportedVisibilityLocked();
    }
@@ -439,8 +439,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
    }

    void onRemovedFromDisplay() {
        AppWindowToken startingToken = null;

        if (DEBUG_APP_TRANSITIONS) Slog.v(TAG_WM, "Removing app token: " + this);

        boolean delayed = setVisibility(null, false, TRANSIT_UNSET, true, mVoiceInteraction);
@@ -461,6 +459,10 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
        if (DEBUG_ADD_REMOVE || DEBUG_TOKEN_MOVEMENT) Slog.v(TAG_WM, "removeAppToken: "
                + this + " delayed=" + delayed + " Callers=" + Debug.getCallers(4));

        if (startingData != null && getController() != null) {
            getController().removeStartingWindow();
        }

        final TaskStack stack = mTask.mStack;
        if (delayed && !isEmpty()) {
            // set the token aside because it has an active animation to be finished
@@ -477,9 +479,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
        }

        removed = true;
        if (startingData != null) {
            startingToken = this;
        }
        stopFreezingScreen(true, true);
        if (mService.mFocusedApp == this) {
            if (DEBUG_FOCUS_LIGHT) Slog.v(TAG_WM, "Removing focused app token:" + this);
@@ -491,9 +490,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
        if (!delayed) {
            updateReportedVisibilityLocked();
        }

        // Will only remove if startingToken non null.
        mService.scheduleRemoveStartingWindowLocked(startingToken);
    }

    void clearAnimatingFlags() {
@@ -557,7 +553,9 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
        mAppStopped = true;
        destroySurfaces();
        // Remove any starting window that was added for this app if they are still around.
        mTask.mService.scheduleRemoveStartingWindowLocked(this);
        if (getController() != null) {
            getController().removeStartingWindow();
        }
    }

    /**
@@ -667,16 +665,20 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
        // TODO: Something smells about the code below...Is there a better way?
        if (startingWindow == win) {
            if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Notify removed startingWindow " + win);
            mService.scheduleRemoveStartingWindowLocked(this);
            if (getController() != null) {
                getController().removeStartingWindow();
            }
        } else if (mChildren.size() == 0 && startingData != null) {
            // If this is the last window and we had requested a starting transition window,
            // well there is no point now.
            if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Nulling last startingWindow");
            startingData = null;
        } else if (mChildren.size() == 1 && startingView != null) {
        } else if (mChildren.size() == 1 && startingSurface != null) {
            // If this is the last window except for a starting transition window,
            // we need to get rid of the starting transition.
            mService.scheduleRemoveStartingWindowLocked(this);
            if (getController() != null) {
                getController().removeStartingWindow();
            }
        }
    }

@@ -1015,7 +1017,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
        }

        final WindowState tStartingWindow = fromToken.startingWindow;
        if (tStartingWindow != null && fromToken.startingView != null) {
        if (tStartingWindow != null && fromToken.startingSurface != null) {
            // In this case, the starting icon has already been displayed, so start
            // letting windows get shown immediately without any more transitions.
            mService.mSkipAppTransitionAnimation = true;
@@ -1027,13 +1029,13 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree

            // Transfer the starting window over to the new token.
            startingData = fromToken.startingData;
            startingView = fromToken.startingView;
            startingSurface = fromToken.startingSurface;
            startingDisplayed = fromToken.startingDisplayed;
            fromToken.startingDisplayed = false;
            startingWindow = tStartingWindow;
            reportedVisible = fromToken.reportedVisible;
            fromToken.startingData = null;
            fromToken.startingView = null;
            fromToken.startingSurface = null;
            fromToken.startingWindow = null;
            fromToken.startingMoved = true;
            tStartingWindow.mToken = this;
@@ -1080,10 +1082,9 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
            startingData = fromToken.startingData;
            fromToken.startingData = null;
            fromToken.startingMoved = true;
            final Message m = mService.mH.obtainMessage(H.ADD_STARTING, this);
            // Note: we really want to do sendMessageAtFrontOfQueue() because we want to process the
            // message ASAP, before any other queued messages.
            mService.mH.sendMessageAtFrontOfQueue(m);
            if (getController() != null) {
                getController().scheduleAddStartingWindow();
            }
            return true;
        }

@@ -1421,10 +1422,10 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
                    pw.print(" firstWindowDrawn="); pw.print(firstWindowDrawn);
                    pw.print(" mIsExiting="); pw.println(mIsExiting);
        }
        if (startingWindow != null || startingView != null
        if (startingWindow != null || startingSurface != null
                || startingDisplayed || startingMoved) {
            pw.print(prefix); pw.print("startingWindow="); pw.print(startingWindow);
                    pw.print(" startingView="); pw.print(startingView);
                    pw.print(" startingSurface="); pw.print(startingSurface);
                    pw.print(" startingDisplayed="); pw.print(startingDisplayed);
                    pw.print(" startingMoved="); pw.println(startingMoved);
        }
Loading