Merge "Enable experimentation on notification snooze options" into oc-mr1-dev
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 54f587e..b1a2133 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10839,6 +10839,26 @@
          */
         public static final String ENABLE_DELETION_HELPER_NO_THRESHOLD_TOGGLE =
                 "enable_deletion_helper_no_threshold_toggle";
+
+        /**
+         * The list of snooze options for notifications
+         * This is encoded as a key=value list, separated by commas. Ex:
+         *
+         * "default=60,options_array=15:30:60:120"
+         *
+         * The following keys are supported:
+         *
+         * <pre>
+         * default               (int)
+         * options_array         (string)
+         * </pre>
+         *
+         * All delays in integer minutes. Array order is respected.
+         * Options will be used in order up to the maximum allowed by the UI.
+         * @hide
+         */
+        public static final String NOTIFICATION_SNOOZE_OPTIONS =
+                "notification_snooze_options";
     }
 
     /**
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 43e762d3..7d9c50d 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -277,6 +277,7 @@
                     Settings.Global.NEW_CONTACT_AGGREGATOR,
                     Settings.Global.NITZ_UPDATE_DIFF,
                     Settings.Global.NITZ_UPDATE_SPACING,
+                    Settings.Global.NOTIFICATION_SNOOZE_OPTIONS,
                     Settings.Global.NSD_ON,
                     Settings.Global.NTP_SERVER,
                     Settings.Global.NTP_TIMEOUT,
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 87f6306..8a1e0b9 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -422,4 +422,14 @@
          increase the rate of unintentional unlocks. -->
     <bool name="config_lockscreenAntiFalsingClassifierEnabled">true</bool>
 
+    <!-- Snooze: default notificaiton snooze time. -->
+    <integer name="config_notification_snooze_time_default">60</integer>
+
+    <!-- Snooze: List of snooze values in integer minutes. -->
+    <integer-array name="config_notification_snooze_times">
+        <item>15</item>
+        <item>30</item>
+        <item>60</item>
+        <item>120</item>
+    </integer-array>
 </resources>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 2148c80..e5f8029 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -82,10 +82,10 @@
 
     <!-- Accessibility actions for the notification menu -->
     <item type="id" name="action_snooze_undo"/>
-    <item type="id" name="action_snooze_15_min"/>
-    <item type="id" name="action_snooze_30_min"/>
-    <item type="id" name="action_snooze_1_hour"/>
-    <item type="id" name="action_snooze_2_hours"/>
+    <item type="id" name="action_snooze_shorter"/>
+    <item type="id" name="action_snooze_short"/>
+    <item type="id" name="action_snooze_long"/>
+    <item type="id" name="action_snooze_longer"/>
     <item type="id" name="action_snooze_assistant_suggestion_1"/>
 </resources>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationSnooze.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationSnooze.java
index c45ca54..54e9ed9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationSnooze.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationSnooze.java
@@ -16,8 +16,10 @@
  */
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
 
@@ -29,11 +31,13 @@
 import android.content.res.Resources;
 import android.graphics.Typeface;
 import android.os.Bundle;
+import android.provider.Settings;
 import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 import android.text.SpannableString;
 import android.text.style.StyleSpan;
 import android.util.AttributeSet;
+import android.util.KeyValueListParser;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -51,11 +55,14 @@
 public class NotificationSnooze extends LinearLayout
         implements NotificationGuts.GutsContent, View.OnClickListener {
 
+    private static final String TAG = "NotificationSnooze";
     /**
      * If this changes more number increases, more assistant action resId's should be defined for
      * accessibility purposes, see {@link #setSnoozeOptions(List)}
      */
     private static final int MAX_ASSISTANT_SUGGESTIONS = 1;
+    private static final String KEY_DEFAULT_SNOOZE = "default";
+    private static final String KEY_OPTIONS = "options_array";
     private NotificationGuts mGutsContainer;
     private NotificationSwipeActionHelper mSnoozeListener;
     private StatusBarNotification mSbn;
@@ -72,9 +79,29 @@
     private boolean mSnoozing;
     private boolean mExpanded;
     private AnimatorSet mExpandAnimation;
+    private KeyValueListParser mParser;
+
+    private final static int[] sAccessibilityActions = {
+            R.id.action_snooze_shorter,
+            R.id.action_snooze_short,
+            R.id.action_snooze_long,
+            R.id.action_snooze_longer,
+    };
 
     public NotificationSnooze(Context context, AttributeSet attrs) {
         super(context, attrs);
+        mParser = new KeyValueListParser(',');
+    }
+
+    @VisibleForTesting
+    SnoozeOption getDefaultOption()
+    {
+        return mDefaultOption;
+    }
+
+    @VisibleForTesting
+    void setKeyValueListParser(KeyValueListParser parser) {
+        mParser = parser;
     }
 
     @Override
@@ -172,17 +199,49 @@
         mSbn = sbn;
     }
 
-    private ArrayList<SnoozeOption> getDefaultSnoozeOptions() {
+    @VisibleForTesting
+    ArrayList<SnoozeOption> getDefaultSnoozeOptions() {
+        final Resources resources = getContext().getResources();
         ArrayList<SnoozeOption> options = new ArrayList<>();
+        try {
+            final String config = Settings.Global.getString(getContext().getContentResolver(),
+                    Settings.Global.NOTIFICATION_SNOOZE_OPTIONS);
+            mParser.setString(config);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Bad snooze constants");
+        }
 
-        options.add(createOption(15 /* minutes */, R.id.action_snooze_15_min));
-        options.add(createOption(30 /* minutes */, R.id.action_snooze_30_min));
-        mDefaultOption = createOption(60 /* minutes */, R.id.action_snooze_1_hour);
-        options.add(mDefaultOption);
-        options.add(createOption(60 * 2 /* minutes */, R.id.action_snooze_2_hours));
+        final int defaultSnooze = mParser.getInt(KEY_DEFAULT_SNOOZE,
+                resources.getInteger(R.integer.config_notification_snooze_time_default));
+        final int[] snoozeTimes = parseIntArray(KEY_OPTIONS,
+                resources.getIntArray(R.array.config_notification_snooze_times));
+
+        for (int i = 0; i < snoozeTimes.length && i < sAccessibilityActions.length; i++) {
+            int snoozeTime = snoozeTimes[i];
+            SnoozeOption option = createOption(snoozeTime, sAccessibilityActions[i]);
+            if (i == 0 || snoozeTime == defaultSnooze) {
+                mDefaultOption = option;
+            }
+            options.add(option);
+        }
         return options;
     }
 
+    @VisibleForTesting
+    int[] parseIntArray(final String key, final int[] defaultArray) {
+        final String value = mParser.getString(key, null);
+        if (value != null) {
+            try {
+                return Arrays.stream(value.split(":")).map(String::trim).mapToInt(
+                        Integer::parseInt).toArray();
+            } catch (NumberFormatException e) {
+                return defaultArray;
+            }
+        } else {
+            return defaultArray;
+        }
+    }
+
     private SnoozeOption createOption(int minutes, int accessibilityActionId) {
         Resources res = getResources();
         boolean showInHours = minutes >= 60;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationSnoozeTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationSnoozeTest.java
new file mode 100644
index 0000000..6b31c96
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationSnoozeTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import android.provider.Settings;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableResources;
+import android.testing.UiThreadTest;
+import android.util.KeyValueListParser;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@UiThreadTest
+public class NotificationSnoozeTest extends SysuiTestCase {
+    private static final int RES_DEFAULT = 2;
+    private static final int[] RES_OPTIONS = {1, 2, 3};
+    private NotificationSnooze mNotificationSnooze;
+    private KeyValueListParser mMockParser;
+
+    @Before
+    public void setUp() throws Exception {
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.NOTIFICATION_SNOOZE_OPTIONS, null);
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(R.integer.config_notification_snooze_time_default, RES_DEFAULT);
+        resources.addOverride(R.array.config_notification_snooze_times, RES_OPTIONS);
+        mNotificationSnooze = new NotificationSnooze(mContext, null);
+        mMockParser = mock(KeyValueListParser.class);
+    }
+
+    @Test
+    public void testParseIntArrayNull() throws Exception {
+        when(mMockParser.getString(anyString(), isNull())).thenReturn(null);
+        mNotificationSnooze.setKeyValueListParser(mMockParser);
+
+        int[] result = mNotificationSnooze.parseIntArray("foo", RES_OPTIONS);
+        assertEquals(RES_OPTIONS, result);
+    }
+
+    @Test
+    public void testParseIntArrayLeadingSep() throws Exception {
+        when(mMockParser.getString(anyString(), isNull())).thenReturn(":4:5:6");
+        mNotificationSnooze.setKeyValueListParser(mMockParser);
+
+        int[] result = mNotificationSnooze.parseIntArray("foo", RES_OPTIONS);
+        assertEquals(RES_OPTIONS, result);
+    }
+
+    @Test
+    public void testParseIntArrayEmptyItem() throws Exception {
+        when(mMockParser.getString(anyString(), isNull())).thenReturn("4::6");
+        mNotificationSnooze.setKeyValueListParser(mMockParser);
+
+        int[] result = mNotificationSnooze.parseIntArray("foo", RES_OPTIONS);
+        assertEquals(RES_OPTIONS, result);
+    }
+
+    @Test
+    public void testParseIntArrayTrailingSep() throws Exception {
+        when(mMockParser.getString(anyString(), isNull())).thenReturn("4:5:6:");
+        mNotificationSnooze.setKeyValueListParser(mMockParser);
+
+        int[] result = mNotificationSnooze.parseIntArray("foo", RES_OPTIONS);
+        assertEquals(3, result.length);
+        assertEquals(4, result[0]);  // respect order
+        assertEquals(5, result[1]);
+        assertEquals(6, result[2]);
+    }
+
+    @Test
+    public void testParseIntArrayGoodData() throws Exception {
+        when(mMockParser.getString(anyString(), isNull())).thenReturn("4:5:6");
+        mNotificationSnooze.setKeyValueListParser(mMockParser);
+
+        int[] result = mNotificationSnooze.parseIntArray("foo", RES_OPTIONS);
+        assertEquals(3, result.length);
+        assertEquals(4, result[0]);  // respect order
+        assertEquals(5, result[1]);
+        assertEquals(6, result[2]);
+    }
+
+    @Test
+    public void testGetOptionsWithNoConfig() throws Exception {
+        ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions();
+        assertEquals(3, result.size());
+        assertEquals(1, result.get(0).getMinutesToSnoozeFor());  // respect order
+        assertEquals(2, result.get(1).getMinutesToSnoozeFor());
+        assertEquals(3, result.get(2).getMinutesToSnoozeFor());
+        assertEquals(2, mNotificationSnooze.getDefaultOption().getMinutesToSnoozeFor());
+    }
+
+    @Test
+    public void testGetOptionsWithInvalidConfig() throws Exception {
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.NOTIFICATION_SNOOZE_OPTIONS,
+                "this is garbage");
+        ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions();
+        assertEquals(3, result.size());
+        assertEquals(1, result.get(0).getMinutesToSnoozeFor());  // respect order
+        assertEquals(2, result.get(1).getMinutesToSnoozeFor());
+        assertEquals(3, result.get(2).getMinutesToSnoozeFor());
+        assertEquals(2, mNotificationSnooze.getDefaultOption().getMinutesToSnoozeFor());
+    }
+
+    @Test
+    public void testGetOptionsWithValidDefault() throws Exception {
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.NOTIFICATION_SNOOZE_OPTIONS,
+                "default=10,options_array=4:5:6:7");
+        ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions();
+        assertNotNull(mNotificationSnooze.getDefaultOption());  // pick one
+    }
+
+    @Test
+    public void testGetOptionsWithValidConfig() throws Exception {
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.NOTIFICATION_SNOOZE_OPTIONS,
+                "default=6,options_array=4:5:6:7");
+        ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions();
+        assertEquals(4, result.size());
+        assertEquals(4, result.get(0).getMinutesToSnoozeFor());  // respect order
+        assertEquals(5, result.get(1).getMinutesToSnoozeFor());
+        assertEquals(6, result.get(2).getMinutesToSnoozeFor());
+        assertEquals(7, result.get(3).getMinutesToSnoozeFor());
+        assertEquals(6, mNotificationSnooze.getDefaultOption().getMinutesToSnoozeFor());
+    }
+
+    @Test
+    public void testGetOptionsWithLongConfig() throws Exception {
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.NOTIFICATION_SNOOZE_OPTIONS,
+                "default=6,options_array=4:5:6:7:8:9:10:11:12:13:14:15:16:17");
+        ArrayList<SnoozeOption> result = mNotificationSnooze.getDefaultSnoozeOptions();
+        assertTrue(result.size() > 3);
+        assertEquals(4, result.get(0).getMinutesToSnoozeFor());  // respect order
+        assertEquals(5, result.get(1).getMinutesToSnoozeFor());
+        assertEquals(6, result.get(2).getMinutesToSnoozeFor());
+    }
+}