Replace smart reply boolean setting with key-value list

This patch replaces the recently introduced
Settings.Global.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS boolean setting
with a new Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS
key-value list.

Rationale: This will allow us to add and tweak smart reply parameters
without polluting the global settings namespace.

Bug: 67765414
Test: atest SmartReplyConstantsTest
Change-Id: I284bb6b31618a234c4772d16ad6190a713035f1b
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 84996e0..a183895 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12089,11 +12089,22 @@
                 "zram_enabled";
 
         /**
-         * Whether smart replies in notifications are enabled.
+         * Configuration flags for smart replies in notifications.
+         * This is encoded as a key=value list, separated by commas. Ex:
+         *
+         * "enabled=1,max_squeeze_remeasure_count=3"
+         *
+         * The following keys are supported:
+         *
+         * <pre>
+         * enabled                         (boolean)
+         * max_squeeze_remeasure_attempts  (int)
+         * </pre>
+         * @see com.android.systemui.statusbar.policy.SmartReplyConstants
          * @hide
          */
-        public static final String ENABLE_SMART_REPLIES_IN_NOTIFICATIONS =
-                "enable_smart_replies_in_notifications";
+        public static final String SMART_REPLIES_IN_NOTIFICATIONS_FLAGS =
+                "smart_replies_in_notifications_flags";
 
         /**
          * If nonzero, crashes in foreground processes will bring up a dialog.
diff --git a/core/proto/android/providers/settings.proto b/core/proto/android/providers/settings.proto
index ff0f4fd..f2b7ab2 100644
--- a/core/proto/android/providers/settings.proto
+++ b/core/proto/android/providers/settings.proto
@@ -422,7 +422,7 @@
     optional SettingProto notification_snooze_options = 341 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto enable_gnss_raw_meas_full_tracking = 346 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto zram_enabled = 347 [ (android.privacy).dest = DEST_AUTOMATIC ];
-    optional SettingProto enable_smart_replies_in_notifications = 348 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    optional SettingProto smart_replies_in_notifications_flags = 348 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto show_first_crash_dialog = 349 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto show_restart_in_crash_dialog = 351 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto show_mute_in_crash_dialog = 352 [ (android.privacy).dest = DEST_AUTOMATIC ];
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 853a36d..5bb9abe 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -220,7 +220,7 @@
                     Settings.Global.ENABLE_DELETION_HELPER_NO_THRESHOLD_TOGGLE,
                     Settings.Global.ENABLE_DISKSTATS_LOGGING,
                     Settings.Global.ENABLE_EPHEMERAL_FEATURE,
-                    Settings.Global.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS,
+                    Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS,
                     Settings.Global.ENHANCED_4G_MODE_ENABLED,
                     Settings.Global.EPHEMERAL_COOKIE_MAX_SIZE_BYTES,
                     Settings.Global.ERROR_LOGCAT_PREFIX,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 39f2b52..1551b8e 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1112,8 +1112,8 @@
                 Settings.Global.ZRAM_ENABLED,
                 GlobalSettingsProto.ZRAM_ENABLED);
         dumpSetting(s, p,
-                Settings.Global.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS,
-                GlobalSettingsProto.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS);
+                Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS,
+                GlobalSettingsProto.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS);
         dumpSetting(s, p,
                 Settings.Global.SHOW_FIRST_CRASH_DIALOG,
                 GlobalSettingsProto.SHOW_FIRST_CRASH_DIALOG);
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index f2e5d3b..cf0659a 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -475,4 +475,11 @@
         <item>60</item>
         <item>120</item>
     </integer-array>
+
+    <!-- Smart replies in notifications: Whether smart replies in notifications are enabled. -->
+    <bool name="config_smart_replies_in_notifications_enabled">true</bool>
+
+    <!-- Smart replies in notifications: Maximum number of times SmartReplyView will try to find a
+         better (narrower) line-break for a double-line smart reply button. -->
+    <integer name="config_smart_replies_in_notifications_max_squeeze_remeasure_attempts">3</integer>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index 0f3daf5..d1834e9 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -52,6 +52,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.SmartReplyConstants;
 
 import java.util.function.Consumer;
 
@@ -130,6 +131,8 @@
         providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(context));
         providers.put(NotificationRemoteInputManager.class,
                 () -> new NotificationRemoteInputManager(context));
+        providers.put(SmartReplyConstants.class,
+                () -> new SmartReplyConstants(Dependency.get(Dependency.MAIN_HANDLER), context));
         providers.put(NotificationListener.class, () -> new NotificationListener(context));
         providers.put(NotificationLogger.class, NotificationLogger::new);
         providers.put(NotificationViewHierarchyManager.class,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index e811ed3..c4d0b79 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -22,7 +22,6 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Build;
-import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -36,6 +35,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.notification.HybridGroupManager;
 import com.android.systemui.statusbar.notification.HybridNotificationView;
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.notification.NotificationViewWrapper;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.policy.RemoteInputView;
+import com.android.systemui.statusbar.policy.SmartReplyConstants;
 import com.android.systemui.statusbar.policy.SmartReplyView;
 
 /**
@@ -75,6 +76,8 @@
 
     private RemoteInputView mExpandedRemoteInput;
     private RemoteInputView mHeadsUpRemoteInput;
+
+    private SmartReplyConstants mSmartReplyConstants;
     private SmartReplyView mExpandedSmartReplyView;
 
     private NotificationViewWrapper mContractedWrapper;
@@ -145,6 +148,7 @@
     public NotificationContentView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mHybridGroupManager = new HybridGroupManager(getContext(), this);
+        mSmartReplyConstants = Dependency.get(SmartReplyConstants.class);
         initView();
     }
 
@@ -1166,8 +1170,7 @@
             return;
         }
 
-        boolean enableSmartReplies = Settings.Global.getInt(mContext.getContentResolver(),
-                Settings.Global.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS, 0) != 0;
+        boolean enableSmartReplies = mSmartReplyConstants.isEnabled();
 
         boolean hasRemoteInput = false;
         RemoteInput remoteInputWithChoices = null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java
new file mode 100644
index 0000000..c5067a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2018 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.systemui.statusbar.policy;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
+import android.util.Log;
+
+import com.android.systemui.R;
+
+public final class SmartReplyConstants extends ContentObserver {
+
+    private static final String TAG = "SmartReplyConstants";
+
+    private static final String KEY_ENABLED = "enabled";
+    private static final String KEY_MAX_SQUEEZE_REMEASURE_ATTEMPTS =
+            "max_squeeze_remeasure_attempts";
+
+    private final boolean mDefaultEnabled;
+    private final int mDefaultMaxSqueezeRemeasureAttempts;
+
+    private boolean mEnabled;
+    private int mMaxSqueezeRemeasureAttempts;
+
+    private final Context mContext;
+    private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+    public SmartReplyConstants(Handler handler, Context context) {
+        super(handler);
+
+        mContext = context;
+        final Resources resources = mContext.getResources();
+        mDefaultEnabled = resources.getBoolean(
+                R.bool.config_smart_replies_in_notifications_enabled);
+        mDefaultMaxSqueezeRemeasureAttempts = resources.getInteger(
+                R.integer.config_smart_replies_in_notifications_max_squeeze_remeasure_attempts);
+
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS),
+                false, this);
+        updateConstants();
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        updateConstants();
+    }
+
+    private void updateConstants() {
+        synchronized (SmartReplyConstants.this) {
+            try {
+                mParser.setString(Settings.Global.getString(mContext.getContentResolver(),
+                        Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS));
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Bad smart reply constants", e);
+            }
+            mEnabled = mParser.getBoolean(KEY_ENABLED, mDefaultEnabled);
+            mMaxSqueezeRemeasureAttempts = mParser.getInt(
+                    KEY_MAX_SQUEEZE_REMEASURE_ATTEMPTS, mDefaultMaxSqueezeRemeasureAttempts);
+        }
+    }
+
+    /** Returns whether smart replies in notifications are enabled. */
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    /**
+     * Returns the maximum number of times {@link SmartReplyView#onMeasure(int, int)} will try to
+     * find a better (narrower) line-break for a double-line smart reply button.
+     */
+    public int getMaxSqueezeRemeasureAttempts() {
+        return mMaxSqueezeRemeasureAttempts;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
index 2d829af..57fc03c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
@@ -12,6 +12,7 @@
 import android.widget.Button;
 import android.widget.LinearLayout;
 
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
 
 /** View which displays smart reply buttons in notifications. */
@@ -19,8 +20,11 @@
 
     private static final String TAG = "SmartReplyView";
 
+    private final SmartReplyConstants mConstants;
+
     public SmartReplyView(Context context, AttributeSet attrs) {
         super(context, attrs);
+        mConstants = Dependency.get(SmartReplyConstants.class);
     }
 
     public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java
new file mode 100644
index 0000000..32a7cb9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2018 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.systemui.statusbar.policy;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class SmartReplyConstantsTest extends SysuiTestCase {
+
+    private static final int CONTENT_OBSERVER_TIMEOUT_SECONDS = 10;
+
+    private SmartReplyConstants mConstants;
+
+    @Before
+    public void setUp() {
+        overrideSetting(null); // No config.
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(R.bool.config_smart_replies_in_notifications_enabled, true);
+        resources.addOverride(
+                R.integer.config_smart_replies_in_notifications_max_squeeze_remeasure_attempts, 7);
+        mConstants = new SmartReplyConstants(new Handler(Looper.getMainLooper()), mContext);
+    }
+
+    @Test
+    public void testIsEnabledWithNoConfig() {
+        assertTrue(mConstants.isEnabled());
+    }
+
+    @Test
+    public void testIsEnabledWithInvalidConfig() {
+        overrideSetting("invalid config");
+        triggerConstantsOnChange();
+        assertTrue(mConstants.isEnabled());
+    }
+
+    @Test
+    public void testIsEnabledWithValidConfig() {
+        overrideSetting("enabled=false,max_squeeze_remeasure_attempts=5");
+        triggerConstantsOnChange();
+        assertFalse(mConstants.isEnabled());
+    }
+
+    @Test
+    public void testGetMaxSqueezeRemeasureAttemptsWithNoConfig() {
+        assertTrue(mConstants.isEnabled());
+        assertEquals(7, mConstants.getMaxSqueezeRemeasureAttempts());
+    }
+
+    @Test
+    public void testGetMaxSqueezeRemeasureAttemptsWithInvalidConfig() {
+        overrideSetting("invalid config");
+        triggerConstantsOnChange();
+        assertEquals(7, mConstants.getMaxSqueezeRemeasureAttempts());
+    }
+
+    @Test
+    public void testGetMaxSqueezeRemeasureAttemptsWithValidConfig() {
+        overrideSetting("enabled=false,max_squeeze_remeasure_attempts=5");
+        triggerConstantsOnChange();
+        assertEquals(5, mConstants.getMaxSqueezeRemeasureAttempts());
+    }
+
+    private void overrideSetting(String flags) {
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS, flags);
+    }
+
+    private void triggerConstantsOnChange() {
+        // Since Settings.Global is mocked in TestableContext, we need to manually trigger the
+        // content observer.
+        mConstants.onChange(false,
+                Settings.Global.getUriFor(Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS));
+    }
+}