Don't freeze bindee process if bound with BIND_ALLOW_OOM_MANAGEMENT

Also fixed app freezer's settings change observer, added a device
config item for the debounce timeout, as well as a lock ordering issue

Bug: 183735766
Test: atest FrameworksServicesTests:ActivityManagerTest
Test: atest CachedAppOptimizerTest
Change-Id: Idfdc90fd55172103eb6bb1ab1f64e5c4f17c89c3
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 68f5479..d7fbd49 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -42,6 +42,7 @@
         "androidx.test.ext.truth",
         "androidx.test.runner",
         "androidx.test.rules",
+        "cts-wm-util",
         "platform-compat-test-rules",
         "mockito-target-minus-junit4",
         "platform-test-annotations",
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
index e04841b..6bca5e4 100644
--- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
@@ -46,6 +46,9 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.server.wm.settings.SettingsSession;
 import android.support.test.uiautomator.UiDevice;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.text.TextUtils;
@@ -61,6 +64,8 @@
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Tests for {@link ActivityManager}.
@@ -316,6 +321,102 @@
         }
     }
 
+    @LargeTest
+    @Test
+    public void testAppFreezerWithAllowOomAdj() throws Exception {
+        final long waitFor = 5000;
+        boolean freezerWasEnabled = isFreezerEnabled();
+        SettingsSession<String> freezerEnabled = null;
+        SettingsSession<String> amConstantsSettings = null;
+        DeviceConfigSession<Long> freezerDebounceTimeout = null;
+        MyServiceConnection autoConnection = null;
+        try {
+            if (!freezerWasEnabled) {
+                freezerEnabled = new SettingsSession<>(
+                        Settings.Global.getUriFor(Settings.Global.CACHED_APPS_FREEZER_ENABLED),
+                        Settings.Global::getString, Settings.Global::putString);
+                freezerEnabled.set("enabled");
+                Thread.sleep(waitFor);
+                if (!isFreezerEnabled()) {
+                    // Still not enabled? Probably because the device doesn't support it.
+                    return;
+                }
+            }
+            freezerDebounceTimeout = new DeviceConfigSession<>(
+                    DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                    CachedAppOptimizer.KEY_FREEZER_DEBOUNCE_TIMEOUT,
+                    DeviceConfig::getLong, CachedAppOptimizer.DEFAULT_FREEZER_DEBOUNCE_TIMEOUT);
+            freezerDebounceTimeout.set(waitFor);
+
+            final String activityManagerConstants = Settings.Global.ACTIVITY_MANAGER_CONSTANTS;
+            amConstantsSettings = new SettingsSession<>(
+                Settings.Global.getUriFor(activityManagerConstants),
+                Settings.Global::getString, Settings.Global::putString);
+
+            amConstantsSettings.set(
+                    ActivityManagerConstants.KEY_MAX_SERVICE_INACTIVITY + "=" + waitFor);
+
+            final Intent intent = new Intent();
+            intent.setClassName(TEST_APP, TEST_CLASS);
+
+            final CountDownLatch latch = new CountDownLatch(1);
+            autoConnection = new MyServiceConnection(latch);
+            mContext.bindService(intent, autoConnection,
+                    Context.BIND_AUTO_CREATE | Context.BIND_ALLOW_OOM_MANAGEMENT);
+            try {
+                assertTrue("Timeout to bind to service " + intent.getComponent(),
+                        latch.await(AWAIT_TIMEOUT, TimeUnit.MILLISECONDS));
+            } catch (InterruptedException e) {
+                fail("Unable to bind to service " + intent.getComponent());
+            }
+            assertFalse(TEST_APP + " shouldn't be frozen now.", isAppFrozen(TEST_APP));
+
+            // Trigger oomAdjUpdate/
+            toggleScreenOn(false);
+            toggleScreenOn(true);
+
+            // Wait for the freezer kick in if there is any.
+            Thread.sleep(waitFor * 4);
+
+            // It still shouldn't be frozen, although it's been in cached state.
+            assertFalse(TEST_APP + " shouldn't be frozen now.", isAppFrozen(TEST_APP));
+        } finally {
+            toggleScreenOn(true);
+            if (amConstantsSettings != null) {
+                amConstantsSettings.close();
+            }
+            if (freezerEnabled != null) {
+                freezerEnabled.close();
+            }
+            if (freezerDebounceTimeout != null) {
+                freezerDebounceTimeout.close();
+            }
+            if (autoConnection != null) {
+                mContext.unbindService(autoConnection);
+            }
+        }
+    }
+
+    private boolean isFreezerEnabled() throws Exception {
+        final String output = runShellCommand("dumpsys activity settings");
+        final Matcher matcher = Pattern.compile("\\b" + CachedAppOptimizer.KEY_USE_FREEZER
+                + "\\b=\\b(true|false)\\b").matcher(output);
+        if (matcher.find()) {
+            return Boolean.parseBoolean(matcher.group(1));
+        }
+        return false;
+    }
+
+    private boolean isAppFrozen(String packageName) throws Exception {
+        final String output = runShellCommand("dumpsys activity p " + packageName);
+        final Matcher matcher = Pattern.compile("\\b" + ProcessCachedOptimizerRecord.IS_FROZEN
+                + "\\b=\\b(true|false)\\b").matcher(output);
+        if (matcher.find()) {
+            return Boolean.parseBoolean(matcher.group(1));
+        }
+        return false;
+    }
+
     /**
      * Make sure the screen state.
      */
diff --git a/services/tests/servicestests/src/com/android/server/am/DeviceConfigSession.java b/services/tests/servicestests/src/com/android/server/am/DeviceConfigSession.java
new file mode 100644
index 0000000..03cf17c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/DeviceConfigSession.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 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.am;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.internal.util.function.TriFunction;
+
+/**
+ * An utility class to set/restore the given device_config item.
+ */
+public class DeviceConfigSession<T> implements AutoCloseable {
+    private final TriFunction<String, String, T, T> mGetter;
+
+    private final String mNamespace;
+    private final String mKey;
+    private final T mInitialValue;
+    private final T mDefaultValue;
+    private boolean mHasInitalValue;
+
+    DeviceConfigSession(String namespace, String key,
+            TriFunction<String, String, T, T> getter, T defaultValue) {
+        mNamespace = namespace;
+        mKey = key;
+        mGetter = getter;
+        mDefaultValue = defaultValue;
+        // Try {@DeviceConfig#getString} firstly since the DeviceConfig API doesn't
+        // support "not found" exception.
+        final String initialStringValue = DeviceConfig.getString(namespace, key, null);
+        if (initialStringValue == null) {
+            mHasInitalValue = false;
+            mInitialValue = defaultValue;
+        } else {
+            mHasInitalValue = true;
+            mInitialValue = getter.apply(namespace, key, defaultValue);
+        }
+    }
+
+    public void set(final @NonNull T value) {
+        DeviceConfig.setProperty(mNamespace, mKey,
+                value == null ? null : value.toString(), false);
+    }
+
+    public T get() {
+        return mGetter.apply(mNamespace, mKey, mDefaultValue);
+    }
+
+    @Override
+    public void close() throws Exception {
+        if (mHasInitalValue) {
+            set(mInitialValue);
+        } else {
+            SystemUtil.runShellCommand("device_config delete " + mNamespace + " " + mKey);
+        }
+    }
+}