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/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 1839e2a..e4cb15f 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -23,6 +23,8 @@
 import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.app.ApplicationExitInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
 import android.os.Debug;
 import android.os.Handler;
 import android.os.Message;
@@ -80,6 +82,8 @@
             "compact_full_delta_rss_throttle_kb";
     @VisibleForTesting static final String KEY_COMPACT_PROC_STATE_THROTTLE =
             "compact_proc_state_throttle";
+    @VisibleForTesting static final String KEY_FREEZER_DEBOUNCE_TIMEOUT =
+            "freeze_debounce_timeout";
 
     // Phenotype sends int configurations and we map them to the strings we'll use on device,
     // preventing a weird string value entering the kernel.
@@ -116,6 +120,10 @@
     // Format of this string should be a comma separated list of integers.
     @VisibleForTesting static final String DEFAULT_COMPACT_PROC_STATE_THROTTLE =
             String.valueOf(ActivityManager.PROCESS_STATE_RECEIVER);
+    @VisibleForTesting static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 600_000L;
+
+    @VisibleForTesting static final Uri CACHED_APP_FREEZER_ENABLED_URI = Settings.Global.getUriFor(
+                Settings.Global.CACHED_APPS_FREEZER_ENABLED);
 
     @VisibleForTesting
     interface PropertyChangedCallbackForTest {
@@ -141,9 +149,6 @@
     static final int SET_FROZEN_PROCESS_MSG = 3;
     static final int REPORT_UNFREEZE_MSG = 4;
 
-    //TODO:change this static definition into a configurable flag.
-    static final long FREEZE_TIMEOUT_MS = 600000;
-
     static final int DO_FREEZE = 1;
     static final int REPORT_UNFREEZE = 2;
 
@@ -198,6 +203,8 @@
                                 updateMinOomAdjThrottle();
                             } else if (KEY_COMPACT_THROTTLE_MAX_OOM_ADJ.equals(name)) {
                                 updateMaxOomAdjThrottle();
+                            } else if (KEY_FREEZER_DEBOUNCE_TIMEOUT.equals(name)) {
+                                updateFreezerDebounceTimeout();
                             }
                         }
                     }
@@ -207,6 +214,23 @@
                 }
             };
 
+    private final class SettingsContentObserver extends ContentObserver {
+        SettingsContentObserver() {
+            super(mAm.mHandler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (CACHED_APP_FREEZER_ENABLED_URI.equals(uri)) {
+                synchronized (mPhenotypeFlagLock) {
+                    updateUseFreezer();
+                }
+            }
+        }
+    }
+
+    private final SettingsContentObserver mSettingsObserver;
+
     private final Object mPhenotypeFlagLock = new Object();
 
     // Configured by phenotype. Updates from the server take effect immediately.
@@ -259,6 +283,8 @@
     @GuardedBy("mProcLock")
     private boolean mFreezerOverride = false;
 
+    @VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
+
     // Maps process ID to last compaction statistics for processes that we've fully compacted. Used
     // when evaluating throttles that we only consider for "full" compaction, so we don't store
     // data for "some" compactions. Uses LinkedHashMap to ensure insertion order is kept and
@@ -293,6 +319,7 @@
         mProcStateThrottle = new HashSet<>();
         mProcessDependencies = processDependencies;
         mTestCallback = callback;
+        mSettingsObserver = new SettingsContentObserver();
     }
 
     /**
@@ -303,6 +330,8 @@
         // TODO: initialize flags to default and only update them if values are set in DeviceConfig
         DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                 ActivityThread.currentApplication().getMainExecutor(), mOnFlagsChangedListener);
+        mAm.mContext.getContentResolver().registerContentObserver(
+                CACHED_APP_FREEZER_ENABLED_URI, false, mSettingsObserver);
         synchronized (mPhenotypeFlagLock) {
             updateUseCompaction();
             updateCompactionActions();
@@ -315,6 +344,7 @@
             updateUseFreezer();
             updateMinOomAdjThrottle();
             updateMaxOomAdjThrottle();
+            updateFreezerDebounceTimeout();
         }
     }
 
@@ -367,6 +397,7 @@
                     + " processes.");
             pw.println(" " + KEY_USE_FREEZER + "=" + mUseFreezer);
             pw.println("  " + KEY_FREEZER_STATSD_SAMPLE_RATE + "=" + mFreezerStatsdSampleRate);
+            pw.println("  " + KEY_FREEZER_DEBOUNCE_TIMEOUT + "=" + mFreezerDebounceTimeout);
             if (DEBUG_COMPACTION) {
                 for (Map.Entry<Integer, LastCompactionStats> entry
                         : mLastCompactionStats.entrySet()) {
@@ -627,21 +658,28 @@
             mUseFreezer = isFreezerSupported();
         }
 
-        if (mUseFreezer && mFreezeHandler == null) {
-            Slog.d(TAG_AM, "Freezer enabled");
-            enableFreezer(true);
+        final boolean useFreezer = mUseFreezer;
+        // enableFreezer() would need the global ActivityManagerService lock, post it.
+        mAm.mHandler.post(() -> {
+            if (useFreezer) {
+                Slog.d(TAG_AM, "Freezer enabled");
+                enableFreezer(true);
 
-            if (!mCachedAppOptimizerThread.isAlive()) {
-                mCachedAppOptimizerThread.start();
+                if (!mCachedAppOptimizerThread.isAlive()) {
+                    mCachedAppOptimizerThread.start();
+                }
+
+                if (mFreezeHandler == null) {
+                    mFreezeHandler = new FreezeHandler();
+                }
+
+                Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(),
+                        Process.THREAD_GROUP_SYSTEM);
+            } else {
+                Slog.d(TAG_AM, "Freezer disabled");
+                enableFreezer(false);
             }
-
-            mFreezeHandler = new FreezeHandler();
-
-            Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(),
-                    Process.THREAD_GROUP_SYSTEM);
-        } else {
-            enableFreezer(false);
-        }
+        });
     }
 
     @GuardedBy("mPhenotypeFlagLock")
@@ -794,6 +832,16 @@
         }
     }
 
+    @GuardedBy("mPhenotypeFlagLock")
+    private void updateFreezerDebounceTimeout() {
+        mFreezerDebounceTimeout = DeviceConfig.getLong(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_FREEZER_DEBOUNCE_TIMEOUT, DEFAULT_FREEZER_DEBOUNCE_TIMEOUT);
+
+        if (mFreezerDebounceTimeout < 0) {
+            mFullDeltaRssThrottleKb = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
+        }
+    }
+
     private boolean parseProcStateThrottle(String procStateThrottleString) {
         String[] procStates = TextUtils.split(procStateThrottleString, ",");
         mProcStateThrottle.clear();
@@ -818,7 +866,7 @@
         return COMPACT_ACTION_STRING[action];
     }
 
-    // This will ensure app will be out of the freezer for at least FREEZE_TIMEOUT_MS
+    // This will ensure app will be out of the freezer for at least mFreezerDebounceTimeout.
     @GuardedBy("mAm")
     void unfreezeTemporarily(ProcessRecord app) {
         if (mUseFreezer) {
@@ -838,7 +886,7 @@
         mFreezeHandler.sendMessageDelayed(
                 mFreezeHandler.obtainMessage(
                     SET_FROZEN_PROCESS_MSG, DO_FREEZE, 0, app),
-                FREEZE_TIMEOUT_MS);
+                mFreezerDebounceTimeout);
     }
 
     @GuardedBy({"mAm", "mProcLock"})
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 5ae65ef..1ffc53c 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2013,6 +2013,10 @@
                         }
                         String adjType = null;
                         if ((cr.flags&Context.BIND_ALLOW_OOM_MANAGEMENT) != 0) {
+                            // Similar to BIND_WAIVE_PRIORITY, keep it unfrozen.
+                            if (clientAdj < ProcessList.CACHED_APP_MIN_ADJ) {
+                                app.mOptRecord.setShouldNotFreeze(true);
+                            }
                             // Not doing bind OOM management, so treat
                             // this guy more like a started service.
                             if (state.hasShownUi() && !state.getCachedIsHomeProcess()) {
diff --git a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java
index f4ce723..026c1d3 100644
--- a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java
+++ b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java
@@ -17,6 +17,7 @@
 package com.android.server.am;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
 
@@ -28,6 +29,9 @@
 
     private final ActivityManagerGlobalLock mProcLock;
 
+    @VisibleForTesting
+    static final String IS_FROZEN = "isFrozen";
+
     /**
      * The last time that this process was compacted.
      */
@@ -169,5 +173,6 @@
     void dump(PrintWriter pw, String prefix, long nowUptime) {
         pw.print(prefix); pw.print("lastCompactTime="); pw.print(mLastCompactTime);
         pw.print(" lastCompactAction="); pw.println(mLastCompactAction);
+        pw.print(" " + IS_FROZEN + "="); pw.println(mFrozen);
     }
 }
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);
+        }
+    }
+}