Allow setting a persistent VR thread.

Bug: 36215076
Test: adb shell am instrument -w -e class \
android.os.SetPersistentVrThreadTest \
com.android.frameworks.coretests/\
android.support.test.runner.AndroidJUnitRunner
cts-tradefed run cts -m CtsVrTestCases -t android.vr.cts.VrSetFIFOThreadTest
Change-Id: If2f3a5f1c8593c74ac35964f68dcbe75b4da472e
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 043e0ab..8a0af9a 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -4060,6 +4060,10 @@
      * thread can be a VR thread in a process at a time, and that thread may be subject to
      * restrictions on the amount of time it can run.
      *
+     * If persistent VR mode is set, whatever thread has been granted aggressive scheduling via this
+     * method will return to normal operation, and calling this method will do nothing while
+     * persistent VR mode is enabled.
+     *
      * To reset the VR thread for an application, a tid of 0 can be passed.
      *
      * @see android.os.Process#myTid()
@@ -4074,6 +4078,31 @@
     }
 
     /**
+     * Enable more aggressive scheduling for latency-sensitive low-runtime VR threads that persist
+     * beyond a single process. It requires holding the
+     * {@link android.Manifest.permission#RESTRICTED_VR_ACCESS} permission. Only one thread can be a
+     * persistent VR thread at a time, and that thread may be subject to restrictions on the amount
+     * of time it can run. Calling this method will disable aggressive scheduling for non-persistent
+     * VR threads set via {@link #setVrThread}. If persistent VR mode is disabled then the
+     * persistent VR thread loses its new scheduling priority; this method must be called again to
+     * set the persistent thread.
+     *
+     * To reset the persistent VR thread, a tid of 0 can be passed.
+     *
+     * @see android.os.Process#myTid()
+     * @param tid tid of the VR thread
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.RESTRICTED_VR_ACCESS)
+    public static void setPersistentVrThread(int tid) {
+        try {
+            getService().setPersistentVrThread(tid);
+        } catch (RemoteException e) {
+            // pass
+        }
+    }
+
+    /**
      * The AppTask allows you to manage your own application's tasks.
      * See {@link android.app.ActivityManager#getAppTasks()}
      */
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 77edaea..f5e0c38 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -605,6 +605,7 @@
     ActivityManager.TaskSnapshot getTaskSnapshot(int taskId);
 
     void scheduleApplicationInfoChanged(in List<String> packageNames, int userId);
+    void setPersistentVrThread(int tid);
 
     // WARNING: when these transactions are updated, check if they are any callers on the native
     // side. If so, make sure they are using the correct transaction ids and arguments.
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 7b8c229..5669189 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -115,6 +115,9 @@
     <!-- accessibility test permissions -->
     <uses-permission android:name="android.permission.RETRIEVE_WINDOW_CONTENT" />
 
+    <!-- vr test permissions -->
+    <uses-permission android:name="android.permission.RESTRICTED_VR_ACCESS" />
+
     <application android:theme="@style/Theme" android:supportsRtl="true">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
diff --git a/core/tests/coretests/src/android/os/SetPersistentVrThreadTest.java b/core/tests/coretests/src/android/os/SetPersistentVrThreadTest.java
new file mode 100644
index 0000000..920988b
--- /dev/null
+++ b/core/tests/coretests/src/android/os/SetPersistentVrThreadTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 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 android.os;
+
+import android.app.ActivityManager;
+import android.app.VrManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Process;
+import android.provider.Settings;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+/**
+ * Tests ActivityManager#setPersistentVrThread and ActivityManager#setVrThread's
+ * interaction with persistent VR mode.
+ */
+public class SetPersistentVrThreadTest extends ActivityInstrumentationTestCase2<TestVrActivity> {
+    private TestVrActivity mActivity;
+    private ActivityManager mActivityManager;
+    private VrManager mVrManager;
+    private Context mContext;
+    private String mOldVrListener;
+    private ComponentName mRequestedComponent;
+    private static final int SCHED_OTHER = 0;
+    private static final int SCHED_FIFO = 1;
+    private static final int SCHED_RESET_ON_FORK = 0x40000000;
+    public static final String ENABLED_VR_LISTENERS = "enabled_vr_listeners";
+    private static final String TAG = "VrSetPersistentFIFOThreadTest";
+
+    public SetPersistentVrThreadTest() {
+        super(TestVrActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContext = getInstrumentation().getTargetContext();
+        mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+        mVrManager = (VrManager) mContext.getSystemService(Context.VR_SERVICE);
+
+        mRequestedComponent = new ComponentName(mContext,
+                TestVrActivity.TestVrListenerService.class);
+        mOldVrListener = Settings.Secure.getString(mContext.getContentResolver(),
+                ENABLED_VR_LISTENERS);
+        Settings.Secure.putString(mContext.getContentResolver(), ENABLED_VR_LISTENERS,
+                mRequestedComponent.flattenToString());
+        mActivity = getActivity();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        try {
+            setPersistentVrModeEnabled(false);
+        } catch (Throwable e) {
+            // pass
+        }
+        Settings.Secure.putString(mContext.getContentResolver(), ENABLED_VR_LISTENERS,
+                mOldVrListener);
+        super.tearDown();
+    }
+
+    private void setPersistentVrModeEnabled(boolean enable) throws Throwable {
+        mVrManager.setPersistentVrModeEnabled(enable);
+        // Allow the system time to send out callbacks for persistent VR mode.
+        Thread.sleep(200);
+    }
+
+    @SmallTest
+    public void testSetPersistentVrThreadAPISuccess() throws Throwable {
+        if (!mActivity.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) {
+            return;
+        }
+
+        int vr_thread = 0, policy = 0;
+        vr_thread = Process.myTid();
+
+        setPersistentVrModeEnabled(true);
+        mActivityManager.setPersistentVrThread(vr_thread);
+        policy = (int) Process.getThreadScheduler(vr_thread);
+        assertEquals((SCHED_FIFO | SCHED_RESET_ON_FORK), policy);
+
+        // Check that the thread loses priority when persistent mode is disabled.
+        setPersistentVrModeEnabled(false);
+        policy = (int) Process.getThreadScheduler(vr_thread);
+        assertEquals(SCHED_OTHER, policy);
+
+        // Check that disabling VR mode when in persistent mode does not affect the persistent
+        // thread.
+        mActivity.setVrModeEnabled(true, mRequestedComponent);
+        Thread.sleep(200);
+        setPersistentVrModeEnabled(true);
+        Thread.sleep(200);
+        mActivityManager.setPersistentVrThread(vr_thread);
+        policy = (int) Process.getThreadScheduler(vr_thread);
+        assertEquals((SCHED_FIFO | SCHED_RESET_ON_FORK), policy);
+        mActivity.setVrModeEnabled(false, mRequestedComponent);
+        Thread.sleep(200);
+        assertEquals((SCHED_FIFO | SCHED_RESET_ON_FORK), policy);
+        setPersistentVrModeEnabled(false);
+        policy = (int) Process.getThreadScheduler(vr_thread);
+        assertEquals(SCHED_OTHER, policy);
+    }
+
+    @SmallTest
+    public void testSetPersistentVrThreadAPIFailure() throws Throwable {
+        if (!mActivity.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) {
+            return;
+        }
+        int vr_thread = 0, policy = 0;
+        vr_thread = Process.myTid();
+        mActivityManager.setPersistentVrThread(vr_thread);
+        policy = (int) Process.getThreadScheduler(vr_thread);
+        assertEquals(SCHED_OTHER, policy);
+    }
+
+    @SmallTest
+    public void testSetVrThreadAPIFailsInPersistentMode() throws Throwable {
+        if (!mActivity.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) {
+            return;
+        }
+        int vr_thread = 0, policy = 0;
+        mActivity.setVrModeEnabled(true, mRequestedComponent);
+        vr_thread = Process.myTid();
+
+        setPersistentVrModeEnabled(true);
+        mActivityManager.setVrThread(vr_thread);
+        policy = (int) Process.getThreadScheduler(vr_thread);
+        assertEquals(SCHED_OTHER, policy);
+        setPersistentVrModeEnabled(false);
+
+        // When not in persistent mode the API works again.
+        mActivity.setVrModeEnabled(true, mRequestedComponent);
+        mActivityManager.setVrThread(vr_thread);
+        policy = (int) Process.getThreadScheduler(vr_thread);
+        assertEquals((SCHED_FIFO | SCHED_RESET_ON_FORK), policy);
+
+        // The thread loses priority when persistent mode is disabled.
+        setPersistentVrModeEnabled(true);
+        policy = (int) Process.getThreadScheduler(vr_thread);
+        assertEquals(SCHED_OTHER, policy);
+        setPersistentVrModeEnabled(false);
+    }
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b40e709..1649c89 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -368,6 +368,7 @@
 import com.android.server.pm.Installer;
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.vr.PersistentVrStateListener;
 import com.android.server.vr.VrManagerInternal;
 import com.android.server.wm.WindowManagerService;
 
@@ -593,7 +594,70 @@
     // default action automatically.  Important for devices without direct input
     // devices.
     private boolean mShowDialogs = true;
-    private boolean mInVrMode = false;
+    // VR state flags.
+    static final int NON_VR_MODE = 0;
+    static final int VR_MODE = 1;
+    static final int PERSISTENT_VR_MODE = 2;
+    private int mVrState = NON_VR_MODE;
+    private int mTopAppVrThreadTid = 0;
+    private int mPersistentVrThreadTid = 0;
+    final PersistentVrStateListener mPersistentVrModeListener =
+            new PersistentVrStateListener() {
+        @Override
+        public void onPersistentVrStateChanged(boolean enabled) {
+            synchronized(ActivityManagerService.this) {
+                // There are 4 possible cases here:
+                //
+                // Cases for enabled == true
+                // Invariant: mVrState != PERSISTENT_VR_MODE;
+                //    This is guaranteed as only this function sets mVrState to PERSISTENT_VR_MODE
+                // Invariant: mPersistentVrThreadTid == 0
+                //   This is guaranteed by VrManagerService, which only emits callbacks when the
+                //   mode changes, and in setPersistentVrThread, which only sets
+                //   mPersistentVrThreadTid when mVrState = PERSISTENT_VR_MODE
+                // Case 1: mTopAppVrThreadTid > 0 (someone called setVrThread successfully and is
+                // the top-app)
+                //     We reset the app which currently has SCHED_FIFO (mPersistentVrThreadTid) to
+                //     SCHED_OTHER
+                // Case 2: mTopAppVrThreadTid == 0
+                //     Do nothing
+                //
+                // Cases for enabled == false
+                // Invariant: mVrState == PERSISTENT_VR_MODE;
+                //     This is guaranteed by VrManagerService, which only emits callbacks when the
+                //     mode changes, and the only other assignment of mVrState outside of this
+                //     function checks if mVrState != PERSISTENT_VR_MODE
+                // Invariant: mTopAppVrThreadTid == 0
+                //     This is guaranteed in that mTopAppVrThreadTid is only set to a tid when
+                //     mVrState is VR_MODE, and is explicitly set to 0 when setPersistentVrThread is
+                //     called
+                //   mPersistentVrThreadTid > 0 (someone called setPersistentVrThread successfully)
+                //     3. Reset mPersistentVrThreadTidto SCHED_OTHER
+                //   mPersistentVrThreadTid == 0
+                //     4. Do nothing
+                if (enabled) {
+                    mVrState = PERSISTENT_VR_MODE;
+                } else {
+                    // Leaving persistent mode implies leaving VR mode.
+                    mVrState = NON_VR_MODE;
+                }
+
+                if (mVrState == PERSISTENT_VR_MODE) {
+                    if (mTopAppVrThreadTid > 0) {
+                        // Ensure that when entering persistent VR mode the last top-app loses
+                        // SCHED_FIFO.
+                        Process.setThreadScheduler(mTopAppVrThreadTid, Process.SCHED_OTHER, 0);
+                        mTopAppVrThreadTid = 0;
+                    }
+                } else if (mPersistentVrThreadTid > 0) {
+                    // Ensure that when leaving persistent VR mode we reschedule the high priority
+                    // persistent thread.
+                    Process.setThreadScheduler(mPersistentVrThreadTid, Process.SCHED_OTHER, 0);
+                    mPersistentVrThreadTid = 0;
+                }
+            }
+        }
+    };
 
     // Whether we should use SCHED_FIFO for UI and RenderThreads.
     private boolean mUseFifoUiScheduling = false;
@@ -2325,28 +2389,36 @@
                 }
                 final ActivityRecord r = (ActivityRecord) msg.obj;
                 boolean vrMode;
+                boolean inVrMode;
                 ComponentName requestedPackage;
                 ComponentName callingPackage;
                 int userId;
                 synchronized (ActivityManagerService.this) {
                     vrMode = r.requestedVrComponent != null;
+                    inVrMode = mVrState != NON_VR_MODE;
                     requestedPackage = r.requestedVrComponent;
                     userId = r.userId;
                     callingPackage = r.info.getComponentName();
-                    if (mInVrMode != vrMode) {
-                        mInVrMode = vrMode;
-                        mShowDialogs = shouldShowDialogs(getGlobalConfiguration(), mInVrMode);
+                    if (vrMode != inVrMode) {
+                        // Don't change state if we're in persistent VR mode, but do update thread
+                        // priorities if necessary.
+                        if (mVrState != PERSISTENT_VR_MODE) {
+                            mVrState = vrMode ? VR_MODE : NON_VR_MODE;
+                        }
+                        mShowDialogs = shouldShowDialogs(getGlobalConfiguration(), vrMode);
                         if (r.app != null) {
                             ProcessRecord proc = r.app;
                             if (proc.vrThreadTid > 0) {
                                 if (proc.curSchedGroup == ProcessList.SCHED_GROUP_TOP_APP) {
                                     try {
-                                        if (mInVrMode == true) {
+                                        if (mVrState == VR_MODE) {
                                             Process.setThreadScheduler(proc.vrThreadTid,
                                                 Process.SCHED_FIFO | Process.SCHED_RESET_ON_FORK, 1);
+                                            mTopAppVrThreadTid = proc.vrThreadTid;
                                         } else {
                                             Process.setThreadScheduler(proc.vrThreadTid,
                                                 Process.SCHED_OTHER, 0);
+                                            mTopAppVrThreadTid = 0;
                                         }
                                     } catch (IllegalArgumentException e) {
                                         Slog.w(TAG, "Failed to set scheduling policy, thread does"
@@ -13010,54 +13082,101 @@
         }
     }
 
+    @Override
     public void setVrThread(int tid) {
         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_VR_MODE)) {
             throw new UnsupportedOperationException("VR mode not supported on this device!");
         }
 
         synchronized (this) {
+            if (tid > 0 && mVrState == PERSISTENT_VR_MODE) {
+                Slog.e(TAG, "VR thread cannot be set in persistent VR mode!");
+                return;
+            }
             ProcessRecord proc;
             synchronized (mPidsSelfLocked) {
                 final int pid = Binder.getCallingPid();
                 proc = mPidsSelfLocked.get(pid);
-
-                if (proc != null && mInVrMode && tid >= 0) {
-                    // ensure the tid belongs to the process
-                    if (!Process.isThreadInProcess(pid, tid)) {
-                        throw new IllegalArgumentException("VR thread does not belong to process");
-                    }
-
-                    // reset existing VR thread to CFS if this thread still exists and belongs to
-                    // the calling process
-                    if (proc.vrThreadTid != 0
-                            && Process.isThreadInProcess(pid, proc.vrThreadTid)) {
-                        try {
-                            Process.setThreadScheduler(proc.vrThreadTid, Process.SCHED_OTHER, 0);
-                        } catch (IllegalArgumentException e) {
-                            // Ignore this.  Only occurs in race condition where previous VR thread
-                            // was destroyed during this method call.
-                        }
-                    }
-
-                    proc.vrThreadTid = tid;
-
-                    // promote to FIFO now if the tid is non-zero
-                    try {
-                        if (proc.curSchedGroup == ProcessList.SCHED_GROUP_TOP_APP &&
-                            proc.vrThreadTid > 0) {
-                            Process.setThreadScheduler(proc.vrThreadTid,
-                                Process.SCHED_FIFO | Process.SCHED_RESET_ON_FORK, 1);
-                        }
-                    } catch (IllegalArgumentException e) {
-                        Slog.e(TAG, "Failed to set scheduling policy, thread does"
-                               + " not exist:\n" + e);
-                    }
+                if (proc != null && mVrState == VR_MODE && tid >= 0) {
+                    proc.vrThreadTid = updateVrThreadLocked(proc, proc.vrThreadTid, pid, tid);
+                    mTopAppVrThreadTid = proc.vrThreadTid;
                 }
             }
         }
     }
 
     @Override
+    public void setPersistentVrThread(int tid) {
+        if (checkCallingPermission(permission.RESTRICTED_VR_ACCESS) != PERMISSION_GRANTED) {
+            String msg = "Permission Denial: setPersistentVrThread() from pid="
+                    + Binder.getCallingPid()
+                    + ", uid=" + Binder.getCallingUid()
+                    + " requires " + permission.RESTRICTED_VR_ACCESS;
+            Slog.w(TAG, msg);
+            throw new SecurityException(msg);
+        }
+        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_VR_MODE)) {
+            throw new UnsupportedOperationException("VR mode not supported on this device!");
+        }
+
+        synchronized (this) {
+            // Disable any existing VR thread.
+            if (mTopAppVrThreadTid > 0) {
+                Process.setThreadScheduler(mTopAppVrThreadTid, Process.SCHED_OTHER, 0);
+                mTopAppVrThreadTid = 0;
+            }
+
+            if (tid > 0 && mVrState != PERSISTENT_VR_MODE) {
+                Slog.e(TAG, "Persistent VR thread may only be set in persistent VR mode!");
+                return;
+            }
+            ProcessRecord proc;
+            synchronized (mPidsSelfLocked) {
+                final int pid = Binder.getCallingPid();
+                mPersistentVrThreadTid =
+                        updateVrThreadLocked(null, mPersistentVrThreadTid, pid, tid);
+            }
+        }
+    }
+
+    /**
+     * Used by setVrThread and setPersistentVrThread to update a thread's priority. When proc is
+     * non-null it must be in SCHED_GROUP_TOP_APP.  When it is null, the tid is unconditionally
+     * rescheduled.
+     */
+    private int updateVrThreadLocked(ProcessRecord proc, int lastTid, int pid, int tid) {
+        // ensure the tid belongs to the process
+        if (!Process.isThreadInProcess(pid, tid)) {
+            throw new IllegalArgumentException("VR thread does not belong to process");
+        }
+
+        // reset existing VR thread to CFS if this thread still exists and belongs to
+        // the calling process
+        if (lastTid != 0 && Process.isThreadInProcess(pid, lastTid)) {
+            try {
+                Process.setThreadScheduler(lastTid, Process.SCHED_OTHER, 0);
+            } catch (IllegalArgumentException e) {
+                // Ignore this.  Only occurs in race condition where previous VR thread
+                // was destroyed during this method call.
+            }
+        }
+
+        // promote to FIFO now if the tid is non-zero
+        try {
+            if ((proc == null || proc.curSchedGroup == ProcessList.SCHED_GROUP_TOP_APP)
+                    && tid > 0) {
+                Process.setThreadScheduler(tid,
+                    Process.SCHED_FIFO | Process.SCHED_RESET_ON_FORK, 1);
+            }
+            return tid;
+        } catch (IllegalArgumentException e) {
+            Slog.e(TAG, "Failed to set scheduling policy, thread does"
+                   + " not exist:\n" + e);
+        }
+        return lastTid;
+    }
+
+    @Override
     public void setRenderThread(int tid) {
         synchronized (this) {
             ProcessRecord proc;
@@ -13643,7 +13762,10 @@
             mLocalDeviceIdleController
                     = LocalServices.getService(DeviceIdleController.LocalService.class);
             mAssistUtils = new AssistUtils(mContext);
-
+            VrManagerInternal vrManagerInternal = LocalServices.getService(VrManagerInternal.class);
+            if (vrManagerInternal != null) {
+                vrManagerInternal.addPersistentVrModeStateListener(mPersistentVrModeListener);
+            }
             // Make sure we have the current profile info, since it is needed for security checks.
             mUserController.onSystemReady();
             mRecentTasks.onSystemReadyLocked();
@@ -19740,7 +19862,7 @@
                 mUserController.getCurrentUserIdLocked());
 
         // TODO: If our config changes, should we auto dismiss any currently showing dialogs?
-        mShowDialogs = shouldShowDialogs(mTempConfig, mInVrMode);
+        mShowDialogs = shouldShowDialogs(mTempConfig, mVrState != NON_VR_MODE);
 
         AttributeCache ac = AttributeCache.instance();
         if (ac != null) {
@@ -21279,10 +21401,11 @@
                         // do nothing if we already switched to RT
                         if (oldSchedGroup != ProcessList.SCHED_GROUP_TOP_APP) {
                             // Switch VR thread for app to SCHED_FIFO
-                            if (mInVrMode && app.vrThreadTid != 0) {
+                            if (mVrState == VR_MODE && app.vrThreadTid != 0) {
                                 try {
                                     Process.setThreadScheduler(app.vrThreadTid,
                                         Process.SCHED_FIFO | Process.SCHED_RESET_ON_FORK, 1);
+                                    mTopAppVrThreadTid = app.vrThreadTid;
                                 } catch (IllegalArgumentException e) {
                                     // thread died, ignore
                                 }
@@ -21330,6 +21453,7 @@
                         // Safe to do even if we're not in VR mode
                         if (app.vrThreadTid != 0) {
                             Process.setThreadScheduler(app.vrThreadTid, Process.SCHED_OTHER, 0);
+                            mTopAppVrThreadTid = 0;
                         }
                         if (mUseFifoUiScheduling) {
                             // Reset UI pipeline to SCHED_OTHER