Introduce TaskOrganizer

A first take at the TaskOrganizer API for allowing SysUI to control task presentation.
In this CL we introduce the first two primitives:
        1. The interface itself for implementation by SysUI
        2. Support for organizing a given windowing mode (but atm really only PIP)
We include a sample app that manages the PIP from an APPLICATION_OVERLAY window.

Bug: 139371701
Test: wmtests/TaskOrganizerTests. TaskOrganizerPipTest
Change-Id: I44a8ed311bc5f06285bba2c6ff3b37a7d19a9190
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index c959439..9fd3ea4 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -163,6 +163,7 @@
 import android.view.DisplayInfo;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
+import android.view.ITaskOrganizer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -803,6 +804,16 @@
         setWindowingMode(windowingMode, false /* animate */, false /* showRecents */,
                 false /* enteringSplitScreenMode */, false /* deferEnsuringVisibility */,
                 false /* creating */);
+
+        windowingMode = getWindowingMode();
+        /*
+         * Different windowing modes may be managed by different task organizers. If
+         * getTaskOrganizer returns null, we still call transferToTaskOrganizer to
+         * make sure we clear it.
+         */
+        final ITaskOrganizer org =
+            mWmService.mAtmService.mTaskOrganizerController.getTaskOrganizer(windowingMode);
+        transferToTaskOrganizer(org);
     }
 
     /**
@@ -1650,6 +1661,33 @@
     }
 
     /**
+     * Indicate whether the first task in this stack is controlled by a TaskOrganizer. We aren't
+     * expecting to use the TaskOrganizer in multiple task per stack scenarios so checking
+     * the first one is ok.
+     */
+    boolean isControlledByTaskOrganizer() {
+        return getChildCount() > 0 && getTopMostTask().mTaskOrganizer != null;
+    }
+
+    private static void transferSingleTaskToOrganizer(Task tr, ITaskOrganizer organizer) {
+        tr.setTaskOrganizer(organizer);
+    }
+
+    /**
+     * Transfer control of the leashes and IWindowContainers to the given ITaskOrganizer.
+     * This will (or shortly there-after) invoke the taskAppeared callbacks.
+     * If the tasks had a previous TaskOrganizer, setTaskOrganizer will take care of
+     * emitting the taskVanished callbacks.
+     */
+    void transferToTaskOrganizer(ITaskOrganizer organizer) {
+        final PooledConsumer c = PooledLambda.obtainConsumer(
+                ActivityStack::transferSingleTaskToOrganizer,
+                PooledLambda.__(Task.class), organizer);
+        forAllTasks(c);
+        c.recycle();
+    }
+
+    /**
      * Returns true if the stack should be visible.
      *
      * @param starting The currently starting activity or null if there is none.
@@ -3577,6 +3615,15 @@
     void animateResizePinnedStack(Rect toBounds, Rect sourceHintBounds, int animationDuration,
             boolean fromFullscreen) {
         if (!inPinnedWindowingMode()) return;
+
+        /**
+         * TODO(b/146594635): Remove all PIP animation code from WM once SysUI handles animation.
+         * If this PIP Task is controlled by a TaskOrganizer, the animation occurs entirely
+         * on the TaskOrganizer side, so we just hand over the leash without doing any animation.
+         * We have to be careful to not schedule the enter-pip callback as the TaskOrganizer
+         * needs to have flexibility to schedule that at an appropriate point in the animation.
+         */
+        if (isControlledByTaskOrganizer()) return;
         if (toBounds == null /* toFullscreen */) {
             final Configuration parentConfig = getParent().getConfiguration();
             final ActivityRecord top = topRunningNonOverlayTaskActivity();
diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
index e8564fc..2d6eb75 100644
--- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
@@ -2574,6 +2574,7 @@
         final PooledConsumer c = PooledLambda.obtainConsumer(
                 ActivityRecord::updatePictureInPictureMode,
                 PooledLambda.__(ActivityRecord.class), targetStackBounds, forceUpdate);
+        task.getStack().setBounds(targetStackBounds);
         task.forAllActivities(c);
         c.recycle();
     }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 76c0e4e..3548918 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -226,6 +226,7 @@
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 import android.view.IRecentsAnimationRunner;
+import android.view.ITaskOrganizer;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationDefinition;
 import android.view.WindowContainerTransaction;
@@ -662,6 +663,12 @@
 
     private FontScaleSettingObserver mFontScaleSettingObserver;
 
+    /**
+     * Stores the registration and state of TaskOrganizers in use.
+     */
+    TaskOrganizerController mTaskOrganizerController =
+        new TaskOrganizerController(this, mGlobalLock);
+
     private int mDeviceOwnerUid = Process.INVALID_UID;
 
     private final class FontScaleSettingObserver extends ContentObserver {
@@ -1271,6 +1278,14 @@
                 .execute();
     }
 
+    @Override
+    public final void registerTaskOrganizer(ITaskOrganizer organizer, int windowingMode) {
+        enforceCallerIsRecentsOrHasPermission(
+                MANAGE_ACTIVITY_STACKS, "registerTaskOrganizer()");
+        synchronized (mGlobalLock) {
+            mTaskOrganizerController.registerTaskOrganizer(organizer, windowingMode);
+        }
+    }
 
     @Override
     public IBinder requestStartActivityPermissionToken(IBinder delegatorToken) {
@@ -3319,6 +3334,18 @@
         }
     }
 
+    private void applyWindowContainerChange(ConfigurationContainer cc,
+            WindowContainerTransaction.Change c) {
+        sanitizeAndApplyConfigChange(cc, c);
+
+        Rect enterPipBounds = c.getEnterPipBounds();
+        if (enterPipBounds != null) {
+            Task tr = (Task) cc;
+            mStackSupervisor.updatePictureInPictureMode(tr,
+                    enterPipBounds, true);
+        }
+    }
+
     @Override
     public void applyContainerTransaction(WindowContainerTransaction t) {
         mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_STACKS, "applyContainerTransaction()");
@@ -3335,7 +3362,7 @@
                             entries.next();
                     final ConfigurationContainer cc = ConfigurationContainer.RemoteToken.fromBinder(
                             entry.getKey()).getContainer();
-                    sanitizeAndApplyConfigChange(cc, entry.getValue());
+                    applyWindowContainerChange(cc, entry.getValue());
                 }
             }
         } finally {
@@ -4057,7 +4084,11 @@
                     throw new IllegalArgumentException("Stack: " + stack
                             + " doesn't support animated resize.");
                 }
-                if (animate) {
+                /**
+                 * TODO(b/146594635): Remove all PIP animation code from WM
+                 * once SysUI handles animation. Don't even try to animate TaskOrganized tasks.
+                 */
+                if (animate && !stack.isControlledByTaskOrganizer()) {
                     stack.animateResizePinnedStack(null /* destBounds */,
                             null /* sourceHintBounds */, animationDuration,
                             false /* fromFullscreen */);
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index dd3365c..d0310f1 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -666,4 +666,8 @@
             return sb.toString();
         }
     }
+
+    RemoteToken getRemoteToken() {
+        return mRemoteToken;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index c4b67d7..091f66c 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -279,6 +279,19 @@
         // we avoid reintroducing this concept by just choosing one of them here.
         inputWindowHandle.surfaceInset = child.getAttrs().surfaceInsets.left;
 
+        /**
+         * If the window is in a TaskManaged by a TaskOrganizer then most cropping
+         * will be applied using the SurfaceControl hierarchy from the Organizer.
+         * This means we need to make sure that these changes in crop are reflected
+         * in the input windows, and so ensure this flag is set so that
+         * the input crop always reflects the surface hierarchy.
+         * we may have some issues with modal-windows, but I guess we can
+         * cross that bridge when we come to implementing full-screen TaskOrg
+         */
+        if (child.getTask() != null && child.getTask().isControlledByTaskOrganizer()) {
+            inputWindowHandle.replaceTouchableRegionWithCrop(null /* Use this surfaces crop */);
+        }
+
         if (child.mGlobalScale != 1) {
             // If we are scaling the window, input coordinates need
             // to be inversely scaled to map from what is on screen
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index a7bf660..c3e815d 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2168,12 +2168,18 @@
             mService.continueWindowLayout();
         }
 
+        // TODO(b/146594635): Remove all PIP animation code from WM once SysUI handles animation.
         // Notify the pinned stack controller to prepare the PiP animation, expect callback
-        // delivered from SystemUI to WM to start the animation.
-        final PinnedStackController pinnedStackController =
+        // delivered from SystemUI to WM to start the animation. Unless we are using
+        // the TaskOrganizer in which case the animation will be entirely handled
+        // on that side.
+        if (mService.mTaskOrganizerController.getTaskOrganizer(WINDOWING_MODE_PINNED)
+                == null) {
+            final PinnedStackController pinnedStackController =
                 display.mDisplayContent.getPinnedStackController();
-        pinnedStackController.prepareAnimation(sourceHintBounds, aspectRatio,
-                null /* stackBounds */);
+            pinnedStackController.prepareAnimation(sourceHintBounds, aspectRatio,
+                    null /* stackBounds */);
+        }
 
         // TODO: revisit the following statement after the animation is moved from WM to SysUI.
         // Update the visibility of all activities after the they have been reparented to the new
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 9a140da..5cb7091 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -117,9 +117,11 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Debug;
 import android.os.IBinder;
+import android.os.Parcel;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
@@ -130,6 +132,7 @@
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 import android.view.DisplayInfo;
+import android.view.ITaskOrganizer;
 import android.view.RemoteAnimationTarget;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -425,6 +428,14 @@
     }
 
     /**
+     * The TaskOrganizer which is delegated presentation of this task. If set the Task will
+     * emit an IWindowContainer (allowing access to it's SurfaceControl leash) to the organizers
+     * taskAppeared callback, and emit a taskRemoved callback when the Task is vanished.
+     */
+    ITaskOrganizer mTaskOrganizer;
+
+
+    /**
      * Don't use constructor directly. Use {@link #create(ActivityTaskManagerService, int,
      * ActivityInfo, Intent, TaskDescription)} instead.
      */
@@ -445,6 +456,22 @@
                 _voiceSession, _voiceInteractor, stack);
     }
 
+    class TaskToken extends RemoteToken {
+        TaskToken(ConfigurationContainer container) {
+            super(container);
+        }
+
+        @Override
+        public SurfaceControl getLeash() {
+            // We need to copy the SurfaceControl instead of returning the original
+            // because the Parcel FLAGS PARCELABLE_WRITE_RETURN_VALUE cause SurfaceControls
+            // to release themselves.
+            SurfaceControl sc = new SurfaceControl();
+            sc.copyFrom(getSurfaceControl());
+            return sc;
+        }
+    }
+
     /** Don't use constructor directly. This is only used by XML parser. */
     Task(ActivityTaskManagerService atmService, int _taskId, Intent _intent,
             Intent _affinityIntent, String _affinity, String _rootAffinity,
@@ -469,7 +496,7 @@
         mTaskDescription = _lastTaskDescription;
         // Tasks have no set orientation value (including SCREEN_ORIENTATION_UNSPECIFIED).
         setOrientation(SCREEN_ORIENTATION_UNSET);
-        mRemoteToken = new RemoteToken(this);
+        mRemoteToken = new TaskToken(this);
         affinityIntent = _affinityIntent;
         affinity = _affinity;
         rootAffinity = _rootAffinity;
@@ -2179,6 +2206,10 @@
     void removeImmediately() {
         if (DEBUG_STACK) Slog.i(TAG, "removeTask: removing taskId=" + mTaskId);
         EventLogTags.writeWmTaskRemoved(mTaskId, "removeTask");
+
+        // If applicable let the TaskOrganizer know the Task is vanishing.
+        setTaskOrganizer(null);
+
         super.removeImmediately();
     }
 
@@ -2567,6 +2598,12 @@
     }
 
     boolean shouldAnimate() {
+        /**
+         * Animations are handled by the TaskOrganizer implementation.
+         */
+        if (isControlledByTaskOrganizer()) {
+            return false;
+        }
         // Don't animate while the task runs recents animation but only if we are in the mode
         // where we cancel with deferred screenshot, which means that the controller has
         // transformed the task.
@@ -3444,4 +3481,91 @@
             XmlUtils.skipCurrentTag(in);
         }
     }
+
+    boolean isControlledByTaskOrganizer() {
+        return mTaskOrganizer != null;
+    }
+
+    @Override
+    protected void reparentSurfaceControl(SurfaceControl.Transaction t, SurfaceControl newParent) {
+        /**
+         * Avoid yanking back control from the TaskOrganizer, which has presumably reparented the
+         * Surface in to its own hierarchy.
+         */
+        if (isControlledByTaskOrganizer()) {
+            return;
+        }
+        super.reparentSurfaceControl(t, newParent);
+    }
+
+    private void sendTaskAppeared() {
+        if (mSurfaceControl != null && mTaskOrganizer != null) {
+            mAtmService.mTaskOrganizerController.onTaskAppeared(mTaskOrganizer, this);
+        }
+    }
+
+    private void sendTaskVanished() {
+        if (mTaskOrganizer != null) {
+            mAtmService.mTaskOrganizerController.onTaskVanished(mTaskOrganizer, this);
+        }
+   }
+
+    void setTaskOrganizer(ITaskOrganizer organizer) {
+        // Let the old organizer know it has lost control.
+        if (mTaskOrganizer != null) {
+            sendTaskVanished();
+        }
+        mTaskOrganizer = organizer;
+        sendTaskAppeared();
+    }
+
+    // Called on Binder death.
+    void taskOrganizerDied() {
+        mTaskOrganizer = null;
+    }
+
+    @Override
+    void setSurfaceControl(SurfaceControl sc) {
+        super.setSurfaceControl(sc);
+        // If the TaskOrganizer was set before we created the SurfaceControl, we need to
+        // emit the callbacks now.
+        sendTaskAppeared();
+    }
+
+    @Override
+    public void updateSurfacePosition() {
+        // Avoid fighting with the TaskOrganizer over Surface position.
+        if (isControlledByTaskOrganizer()) {
+            getPendingTransaction().setPosition(mSurfaceControl, 0, 0);
+            scheduleAnimation();
+            return;
+        } else {
+            super.updateSurfacePosition();
+        }
+    }
+
+    @Override
+    void getRelativeDisplayedPosition(Point outPos) {
+        // In addition to updateSurfacePosition, we keep other code that sets
+        // position from fighting with the TaskOrganizer
+        if (isControlledByTaskOrganizer()) {
+            outPos.set(0, 0);
+            return;
+        }
+        super.getRelativeDisplayedPosition(outPos);
+    }
+
+    @Override
+    public void setWindowingMode(int windowingMode) {
+        super.setWindowingMode(windowingMode);
+        windowingMode = getWindowingMode();
+        /*
+         * Different windowing modes may be managed by different task organizers. If
+         * getTaskOrganizer returns null, we still call transferToTaskOrganizer to
+         * make sure we clear it.
+         */
+        final ITaskOrganizer org =
+            mAtmService.mTaskOrganizerController.getTaskOrganizer(windowingMode);
+        setTaskOrganizer(org);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
new file mode 100644
index 0000000..283be40
--- /dev/null
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2019 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.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.view.ITaskOrganizer;
+import android.view.SurfaceControl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Stores the TaskOrganizers associated with a given windowing mode and
+ * their associated state.
+ */
+class TaskOrganizerController {
+    private static final String TAG = "TaskOrganizerController";
+
+    private WindowManagerGlobalLock mGlobalLock;
+
+    private class DeathRecipient implements IBinder.DeathRecipient {
+        int mWindowingMode;
+        ITaskOrganizer mTaskOrganizer;
+
+        DeathRecipient(ITaskOrganizer organizer, int windowingMode) {
+            mTaskOrganizer = organizer;
+            mWindowingMode = windowingMode;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mGlobalLock) {
+                final TaskOrganizerState state = mTaskOrganizerStates.get(mTaskOrganizer);
+                for (int i = 0; i < state.mOrganizedTasks.size(); i++) {
+                    state.mOrganizedTasks.get(i).taskOrganizerDied();
+                }
+                mTaskOrganizerStates.remove(mTaskOrganizer);
+                if (mTaskOrganizersForWindowingMode.get(mWindowingMode) == mTaskOrganizer) {
+                    mTaskOrganizersForWindowingMode.remove(mWindowingMode);
+                }
+            }
+        }
+    };
+
+    class TaskOrganizerState {
+        ITaskOrganizer mOrganizer;
+        DeathRecipient mDeathRecipient;
+
+        ArrayList<Task> mOrganizedTasks = new ArrayList<>();
+
+        void addTask(Task t) {
+            mOrganizedTasks.add(t);
+        }
+
+        void removeTask(Task t) {
+            mOrganizedTasks.remove(t);
+        }
+
+        TaskOrganizerState(ITaskOrganizer organizer, DeathRecipient deathRecipient) {
+            mOrganizer = organizer;
+            mDeathRecipient = deathRecipient;
+        }
+    };
+
+
+    final HashMap<Integer, TaskOrganizerState> mTaskOrganizersForWindowingMode = new HashMap();
+    final HashMap<ITaskOrganizer, TaskOrganizerState> mTaskOrganizerStates = new HashMap();
+
+    final HashMap<Integer, ITaskOrganizer> mTaskOrganizersByPendingSyncId = new HashMap();
+
+    final ActivityTaskManagerService mService;
+
+    TaskOrganizerController(ActivityTaskManagerService atm, WindowManagerGlobalLock lock) {
+        mService = atm;
+        mGlobalLock = lock;
+    }
+
+    private void clearIfNeeded(int windowingMode) {
+        final TaskOrganizerState oldState = mTaskOrganizersForWindowingMode.get(windowingMode);
+        if (oldState != null) {
+            oldState.mOrganizer.asBinder().unlinkToDeath(oldState.mDeathRecipient, 0);
+        }
+    }
+
+    /**
+     * Register a TaskOrganizer to manage tasks as they enter the given windowing mode.
+     * If there was already a TaskOrganizer for this windowing mode it will be evicted
+     * and receive taskVanished callbacks in the process.
+     */
+    void registerTaskOrganizer(ITaskOrganizer organizer, int windowingMode) {
+        if (windowingMode != WINDOWING_MODE_PINNED) {
+            throw new UnsupportedOperationException(
+                    "As of now only Pinned windowing mode is supported for registerTaskOrganizer");
+
+        }
+        clearIfNeeded(windowingMode);
+        DeathRecipient dr = new DeathRecipient(organizer, windowingMode);
+        try {
+            organizer.asBinder().linkToDeath(dr, 0);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "TaskOrganizer failed to register death recipient");
+        }
+
+        final TaskOrganizerState state = new TaskOrganizerState(organizer, dr);
+        mTaskOrganizersForWindowingMode.put(windowingMode, state);
+
+        mTaskOrganizerStates.put(organizer, state);
+    }
+
+    ITaskOrganizer getTaskOrganizer(int windowingMode) {
+        final TaskOrganizerState state = mTaskOrganizersForWindowingMode.get(windowingMode);
+        if (state == null) {
+            return null;
+        }
+        return state.mOrganizer;
+    }
+
+    private void sendTaskAppeared(ITaskOrganizer organizer, Task task) {
+        try {
+            organizer.taskAppeared(task.getRemoteToken(), task.getTaskInfo());
+        } catch (Exception e) {
+            Slog.e(TAG, "Exception sending taskAppeared callback" + e);
+        }
+    }
+
+    private void sendTaskVanished(ITaskOrganizer organizer, Task task) {
+        try {
+            organizer.taskVanished(task.getRemoteToken());
+        } catch (Exception e) {
+            Slog.e(TAG, "Exception sending taskVanished callback" + e);
+        }
+    }
+
+    void onTaskAppeared(ITaskOrganizer organizer, Task task) {
+        TaskOrganizerState state = mTaskOrganizerStates.get(organizer);
+
+        state.addTask(task);
+        sendTaskAppeared(organizer, task);
+    }
+
+    void onTaskVanished(ITaskOrganizer organizer, Task task) {
+        final TaskOrganizerState state = mTaskOrganizerStates.get(organizer);
+        sendTaskVanished(organizer, task);
+
+        // This could trigger TaskAppeared for other tasks in the same stack so make sure
+        // we do this AFTER sending taskVanished.
+        state.removeTask(task);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index cefef37..79ffd94 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -342,7 +342,7 @@
         if (mSurfaceControl == null) {
             // If we don't yet have a surface, but we now have a parent, we should
             // build a surface.
-            mSurfaceControl = makeSurface().build();
+            setSurfaceControl(makeSurface().build());
             getPendingTransaction().show(mSurfaceControl);
             updateSurfacePosition();
         } else {
@@ -496,7 +496,7 @@
                 mParent.getPendingTransaction().merge(getPendingTransaction());
             }
 
-            mSurfaceControl = null;
+            setSurfaceControl(null);
             mLastSurfacePosition.set(0, 0);
             scheduleAnimation();
         }
@@ -2209,4 +2209,8 @@
         }
         return mParent.getDimmer();
     }
+
+    void setSurfaceControl(SurfaceControl sc) {
+        mSurfaceControl = sc;
+    }
 }