Merge changes from topic "jr-update" into rvc-dev

* changes:
  Update conversation launch point
  Add settings for apps that don't use full conversations
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 366254f..2bd708a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8385,6 +8385,12 @@
     <!-- [CHAR LIMIT=100] Header for an individual conversation-->
     <string name="conversation_category_title">Conversation</string>
 
+    <!-- [CHAR LIMIT=100] Title for switch that says whether this app can appear in the conversation notification section-->
+    <string name="conversation_section_switch_title">Conversation section</string>
+
+    <!-- [CHAR LIMIT=100] Summary for switch that says whether this app can appear in the conversation notification section-->
+    <string name="conversation_section_switch_summary">Allow <xliff:g id="app">%1$s</xliff:g> to appear in the conversation section</string>
+
     <!-- [CHAR LIMIT=NONE] Conversation preference summary, the parent channel this conversation was spawned from (separator) the parent channel group (e.g. an account name)-->
     <string name="notification_conversation_summary" translatable="false">"<xliff:g id="parent_category_name">%1$s</xliff:g> • <xliff:g id="parent_category_group_name">%2$s</xliff:g>"</string>
 
@@ -8403,6 +8409,15 @@
     <!-- [CHAR LIMIT=100] link to page listing all conversations -->
     <string name="manage_conversations">Manage conversations</string>
 
+    <!-- [CHAR LIMIT=100] summary text on link to 'all conversations' page, no conversations are priority -->
+    <string name="priority_conversation_count_zero">No priority conversations</string>
+
+    <!-- [CHAR LIMIT=100] summary text on link to 'all conversations' page, some conversations are priority -->
+    <plurals name="priority_conversation_count">
+        <item quantity="one"><xliff:g id="count" example="1">%d</xliff:g> priority conversation</item>
+        <item quantity="other"><xliff:g id="count" example="10">%d</xliff:g> priority conversations</item>
+    </plurals>
+
     <!-- [CHAR LIMIT=100] preference category title -->
     <string name="important_conversations">Priority conversations</string>
 
diff --git a/res/xml/app_and_notification.xml b/res/xml/app_and_notification.xml
index b499180..6e71f14 100644
--- a/res/xml/app_and_notification.xml
+++ b/res/xml/app_and_notification.xml
@@ -47,6 +47,14 @@
         android:order="-997"/>
 
     <Preference
+        android:key="conversations"
+        android:title="@string/conversations_category_title"
+        android:order="-550"
+        settings:controller="com.android.settings.notification.ConversationListSummaryPreferenceController"
+        android:fragment="com.android.settings.notification.app.ConversationListSettings"
+    />
+
+    <Preference
         android:key="configure_notification_settings"
         android:title="@string/configure_notification_settings"
         android:order="-440"
diff --git a/res/xml/app_notification_settings.xml b/res/xml/app_notification_settings.xml
index f0200ce..8ca4e0d 100644
--- a/res/xml/app_notification_settings.xml
+++ b/res/xml/app_notification_settings.xml
@@ -29,13 +29,6 @@
     <com.android.settings.notification.app.NotificationFooterPreference
         android:key="block_desc" />
 
-    <!--Bubbles -->
-    <Preference
-        android:key="bubble_pref_link"
-        android:title="@string/notification_bubbles_title"
-        android:icon="@drawable/ic_create_bubble"
-        settings:controller="com.android.settings.notification.app.BubbleSummaryPreferenceController">
-    </Preference>
 
     <!-- Conversations added here -->
     <PreferenceCategory
@@ -43,13 +36,28 @@
         android:key="conversations"
         android:visibility="gone"
         settings:allowDividerAbove="false"
-        settings:allowDividerBelow="false" />
+        settings:allowDividerBelow="false">
+        <com.android.settingslib.RestrictedSwitchPreference
+            android:key="invalid_conversation_switch"
+            android:title="@string/conversation_section_switch_title" />
+        <Preference
+            android:key="invalid_conversation_info"/>
+    </PreferenceCategory>
+
+    <!--Bubbles -->
+    <Preference
+        android:key="bubble_pref_link"
+        android:title="@string/notification_bubbles_title"
+        android:icon="@drawable/ic_create_bubble"
+        settings:allowDividerAbove="false"
+        settings:controller="com.android.settings.notification.app.BubbleSummaryPreferenceController">
+    </Preference>
 
     <!-- Channels/Channel groups added here -->
     <PreferenceCategory
         android:key="channels"
         android:layout="@layout/empty_view"
-        settings:allowDividerAbove="false"
+        settings:allowDividerAbove="true"
         settings:allowDividerBelow="false" />
 
     <!-- Importance toggle -->
diff --git a/res/xml/configure_notification_settings.xml b/res/xml/configure_notification_settings.xml
index 0123ee5..95c7f56 100644
--- a/res/xml/configure_notification_settings.xml
+++ b/res/xml/configure_notification_settings.xml
@@ -39,14 +39,6 @@
     </Preference>
 
     <Preference
-        android:key="conversations"
-        android:title="@string/conversations_category_title"
-        android:summary="@string/manage_conversations"
-        android:order="3"
-        android:fragment="com.android.settings.notification.app.ConversationListSettings"
-    />
-
-    <Preference
         android:key="notification_bubbles"
         android:title="@string/notification_bubbles_title"
         android:summary="@string/notifications_bubble_setting_on_summary"
diff --git a/src/com/android/settings/notification/ConversationListSummaryPreferenceController.java b/src/com/android/settings/notification/ConversationListSummaryPreferenceController.java
new file mode 100644
index 0000000..5d7ea20
--- /dev/null
+++ b/src/com/android/settings/notification/ConversationListSummaryPreferenceController.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 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.settings.notification;
+
+import android.content.Context;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+public class ConversationListSummaryPreferenceController extends BasePreferenceController {
+
+    private NotificationBackend mBackend;
+
+    public ConversationListSummaryPreferenceController(Context context, String key) {
+        super(context, key);
+        mBackend = new NotificationBackend();
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        final int count = mBackend.getConversations(true).getList().size();
+        if (count == 0) {
+            return mContext.getText(R.string.priority_conversation_count_zero);
+        }
+        return mContext.getResources().getQuantityString(
+                R.plurals.priority_conversation_count,
+                count, count);
+    }
+
+    void setBackend(NotificationBackend backend) {
+        mBackend = backend;
+    }
+}
diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java
index 6172268..83df323 100644
--- a/src/com/android/settings/notification/NotificationBackend.java
+++ b/src/com/android/settings/notification/NotificationBackend.java
@@ -268,15 +268,32 @@
         }
     }
 
-    public boolean hasSentMessage(String pkg, int uid) {
+    public boolean isInInvalidMsgState(String pkg, int uid) {
         try {
-            return sINM.hasSentMessage(pkg, uid);
+            return sINM.isInInvalidMsgState(pkg, uid);
         } catch (Exception e) {
             Log.w(TAG, "Error calling NoMan", e);
             return false;
         }
     }
 
+    public boolean hasUserDemotedInvalidMsgApp(String pkg, int uid) {
+        try {
+            return sINM.hasUserDemotedInvalidMsgApp(pkg, uid);
+        } catch (Exception e) {
+            Log.w(TAG, "Error calling NoMan", e);
+            return false;
+        }
+    }
+
+    public void setInvalidMsgAppDemoted(String pkg, int uid, boolean isDemoted) {
+        try {
+             sINM.setInvalidMsgAppDemoted(pkg, uid, isDemoted);
+        } catch (Exception e) {
+            Log.w(TAG, "Error calling NoMan", e);
+        }
+    }
+
     /**
      * Returns all notification channels associated with the package and uid that will bypass DND
      */
diff --git a/src/com/android/settings/notification/app/AppConversationListPreferenceController.java b/src/com/android/settings/notification/app/AppConversationListPreferenceController.java
index 2fcd2b2..0ba9436 100644
--- a/src/com/android/settings/notification/app/AppConversationListPreferenceController.java
+++ b/src/com/android/settings/notification/app/AppConversationListPreferenceController.java
@@ -46,7 +46,7 @@
 
     protected List<ConversationChannelWrapper> mConversations = new ArrayList<>();
     protected PreferenceCategory mPreference;
-    private boolean mHasSentMsg;
+    private boolean mIsInInvalidMsgState;
 
     public AppConversationListPreferenceController(Context context, NotificationBackend backend) {
         super(context, backend);
@@ -88,7 +88,7 @@
         new AsyncTask<Void, Void, Void>() {
             @Override
             protected Void doInBackground(Void... unused) {
-                mHasSentMsg = mBackend.hasSentMessage(mAppRow.pkg, mAppRow.uid);
+                mIsInInvalidMsgState = mBackend.isInInvalidMsgState(mAppRow.pkg, mAppRow.uid);
                 ParceledListSlice<ConversationChannelWrapper> list =
                         mBackend.getConversations(mAppRow.pkg, mAppRow.uid);
                 if (list != null) {
@@ -121,20 +121,10 @@
         if (mPreference == null) {
             return;
         }
-        // TODO: if preference has children, compare with newly loaded list
-        mPreference.removeAll();
-        if (mConversations.isEmpty()) {
-            if (mHasSentMsg) {
-                mPreference.setVisible(true);
-                Preference notSupportedPref = new Preference(mContext);
-                notSupportedPref.setSummary(mContext.getString(
-                        R.string.convo_not_supported_summary, mAppRow.label));
-                mPreference.addPreference(notSupportedPref);
-            } else {
-                mPreference.setVisible(false);
-            }
-        } else {
-            mPreference.setVisible(true);
+
+        if (!mIsInInvalidMsgState && !mConversations.isEmpty()) {
+            // TODO: if preference has children, compare with newly loaded list
+            mPreference.removeAll();
             mPreference.setTitle(getTitleResId());
             populateConversations();
         }
diff --git a/src/com/android/settings/notification/app/AppNotificationSettings.java b/src/com/android/settings/notification/app/AppNotificationSettings.java
index a422841..58c18ba 100644
--- a/src/com/android/settings/notification/app/AppNotificationSettings.java
+++ b/src/com/android/settings/notification/app/AppNotificationSettings.java
@@ -122,6 +122,8 @@
         mControllers.add(new DeletedChannelsPreferenceController(context, mBackend));
         mControllers.add(new ChannelListPreferenceController(context, mBackend));
         mControllers.add(new AppConversationListPreferenceController(context, mBackend));
+        mControllers.add(new InvalidConversationInfoPreferenceController(context, mBackend));
+        mControllers.add(new InvalidConversationPreferenceController(context, mBackend));
         mControllers.add(new BubbleSummaryPreferenceController(context, mBackend));
         return new ArrayList<>(mControllers);
     }
diff --git a/src/com/android/settings/notification/app/BubblePreferenceController.java b/src/com/android/settings/notification/app/BubblePreferenceController.java
index 0ca2095..1aed156 100644
--- a/src/com/android/settings/notification/app/BubblePreferenceController.java
+++ b/src/com/android/settings/notification/app/BubblePreferenceController.java
@@ -86,7 +86,7 @@
     @Override
     public void updateState(Preference preference) {
         if (mIsAppPage && mAppRow != null) {
-            mHasSentInvalidMsg = mBackend.hasSentMessage(mAppRow.pkg, mAppRow.uid);
+            mHasSentInvalidMsg = mBackend.isInInvalidMsgState(mAppRow.pkg, mAppRow.uid);
             mNumConversations = mBackend.getConversations(
                     mAppRow.pkg, mAppRow.uid).getList().size();
             // We're on the app specific bubble page which displays a tri-state
diff --git a/src/com/android/settings/notification/app/InvalidConversationInfoPreferenceController.java b/src/com/android/settings/notification/app/InvalidConversationInfoPreferenceController.java
new file mode 100644
index 0000000..ade9653
--- /dev/null
+++ b/src/com/android/settings/notification/app/InvalidConversationInfoPreferenceController.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 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.settings.notification.app;
+
+import android.app.NotificationChannel;
+import android.content.Context;
+
+import androidx.preference.Preference;
+import androidx.preference.SwitchPreference;
+
+import com.android.settings.R;
+import com.android.settings.notification.NotificationBackend;
+
+public class InvalidConversationInfoPreferenceController extends NotificationPreferenceController {
+
+    private static final String KEY = "invalid_conversation_info";
+
+    public InvalidConversationInfoPreferenceController(Context context,
+            NotificationBackend backend) {
+        super(context, backend);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        if (mAppRow == null) {
+            return false;
+        }
+        if (mAppRow.banned) {
+            return false;
+        }
+        return mBackend.isInInvalidMsgState(mAppRow.pkg, mAppRow.uid);
+    }
+
+    @Override
+    public void updateState(Preference preference) {
+        if (mAppRow == null) {
+            return;
+        }
+        preference.setSummary(mContext.getString(
+                R.string.convo_not_supported_summary, mAppRow.label));
+    }
+}
diff --git a/src/com/android/settings/notification/app/InvalidConversationPreferenceController.java b/src/com/android/settings/notification/app/InvalidConversationPreferenceController.java
new file mode 100644
index 0000000..74f5773
--- /dev/null
+++ b/src/com/android/settings/notification/app/InvalidConversationPreferenceController.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 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.settings.notification.app;
+
+import android.app.NotificationChannel;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.notification.NotificationBackend;
+import com.android.settingslib.RestrictedSwitchPreference;
+
+public class InvalidConversationPreferenceController extends NotificationPreferenceController
+        implements Preference.OnPreferenceChangeListener {
+
+    private static final String KEY = "invalid_conversation_switch";
+
+    public InvalidConversationPreferenceController(Context context, NotificationBackend backend) {
+        super(context, backend);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        if (mAppRow == null) {
+            return false;
+        }
+        if (mAppRow.banned) {
+            return false;
+        }
+        return mBackend.isInInvalidMsgState(mAppRow.pkg, mAppRow.uid);
+    }
+
+    @Override
+    public void updateState(Preference preference) {
+        if (mAppRow == null) {
+            return;
+        }
+        RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference;
+        pref.setDisabledByAdmin(mAdmin);
+        pref.setEnabled(!pref.isDisabledByAdmin());
+        pref.setChecked(!mBackend.hasUserDemotedInvalidMsgApp(mAppRow.pkg, mAppRow.uid));
+        preference.setSummary(mContext.getString(
+                R.string.conversation_section_switch_summary, mAppRow.label));
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        if (mAppRow == null) {
+            return false;
+        }
+        mBackend.setInvalidMsgAppDemoted(mAppRow.pkg, mAppRow.uid, !((Boolean) newValue));
+        return true;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/ConversationListSummaryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/ConversationListSummaryPreferenceControllerTest.java
new file mode 100644
index 0000000..e9b610b
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/ConversationListSummaryPreferenceControllerTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 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.settings.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.service.notification.ConversationChannelWrapper;
+
+import com.android.settings.testutils.shadow.ShadowNotificationBackend;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowNotificationBackend.class)
+public class ConversationListSummaryPreferenceControllerTest {
+
+    private ConversationListSummaryPreferenceController mController;
+    private Context mContext;
+    @Mock
+    NotificationBackend mBackend;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mController = new ConversationListSummaryPreferenceController(mContext, "key");
+        mController.setBackend(mBackend);
+    }
+
+    @Test
+    public void getSummary_noPriorityConversations() {
+        List<ConversationChannelWrapper> convos = new ArrayList<>();
+        when(mBackend.getConversations(true)).thenReturn(
+                new ParceledListSlice<>(convos));
+
+        assertThat(mController.getSummary().toString()).contains("No");
+    }
+
+    @Test
+    public void getSummary_somePriorityConversations() {
+        List<ConversationChannelWrapper> convos = new ArrayList<>();
+        convos.add(mock(ConversationChannelWrapper.class));
+        convos.add(mock(ConversationChannelWrapper.class));
+        when(mBackend.getConversations(true)).thenReturn(
+                new ParceledListSlice<>(convos));
+
+        assertThat(mController.getSummary().toString()).contains("2");
+        assertThat(mController.getSummary().toString()).contains("conversations");
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/app/InvalidConversationInfoPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/app/InvalidConversationInfoPreferenceControllerTest.java
new file mode 100644
index 0000000..aa87539
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/app/InvalidConversationInfoPreferenceControllerTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 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.settings.notification.app;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.UserManager;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.notification.NotificationBackend;
+import com.android.settings.testutils.shadow.SettingsShadowResources;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = SettingsShadowResources.class)
+public class InvalidConversationInfoPreferenceControllerTest {
+
+    private Context mContext;
+    @Mock
+    private NotificationBackend mBackend;
+    @Mock
+    private NotificationManager mNm;
+    @Mock
+    private UserManager mUm;
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private PreferenceScreen mScreen;
+
+    private InvalidConversationInfoPreferenceController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ShadowApplication shadowApplication = ShadowApplication.getInstance();
+        shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
+        shadowApplication.setSystemService(Context.USER_SERVICE, mUm);
+        mContext = RuntimeEnvironment.application;
+        mController = spy(new InvalidConversationInfoPreferenceController(mContext, mBackend));
+    }
+
+    @After
+    public void tearDown() {
+        SettingsShadowResources.reset();
+    }
+
+    @Test
+    public void testNoCrashIfNoOnResume() {
+        mController.isAvailable();
+        mController.updateState(mock(Preference.class));
+    }
+
+    @Test
+    public void testIsAvailable_notIfAppBlocked() {
+        when(mBackend.isInInvalidMsgState(anyString(), anyInt())).thenReturn(true);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "hi";
+        appRow.uid = 0;
+        appRow.banned = true;
+        mController.onResume(appRow, null, null, null, null, null);
+        assertFalse(mController.isAvailable());
+    }
+
+    @Test
+    public void testIsAvailable_notIfInValidMsgState() {
+        when(mBackend.isInInvalidMsgState(anyString(), anyInt())).thenReturn(false);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "hi";
+        appRow.uid = 0;
+        mController.onResume(appRow, null, null, null, null, null);
+        assertFalse(mController.isAvailable());
+    }
+
+    @Test
+    public void testIsAvailable() {
+        when(mBackend.isInInvalidMsgState(anyString(), anyInt())).thenReturn(true);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "hi";
+        appRow.uid = 0;
+        mController.onResume(appRow, null, null, null, null, null);
+        assertTrue(mController.isAvailable());
+    }
+
+    @Test
+    public void testUpdateState() {
+        when(mBackend.isInInvalidMsgState(anyString(), anyInt())).thenReturn(true);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "hi";
+        appRow.uid = 0;
+        appRow.label = "plum";
+        mController.onResume(appRow, null, null, null, null, null);
+        Preference pref = new Preference(mContext);
+        mController.updateState(pref);
+        assertTrue(pref.getSummary().toString().contains(appRow.label));
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/app/InvalidConversationPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/app/InvalidConversationPreferenceControllerTest.java
new file mode 100644
index 0000000..4bfc1b4
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/app/InvalidConversationPreferenceControllerTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2020 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.settings.notification.app;
+
+import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID;
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.UserManager;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.notification.NotificationBackend;
+import com.android.settings.testutils.shadow.SettingsShadowResources;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedSwitchPreference;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = SettingsShadowResources.class)
+public class InvalidConversationPreferenceControllerTest {
+
+    private Context mContext;
+    @Mock
+    private NotificationBackend mBackend;
+    @Mock
+    private NotificationManager mNm;
+    @Mock
+    private UserManager mUm;
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private PreferenceScreen mScreen;
+
+    private InvalidConversationPreferenceController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ShadowApplication shadowApplication = ShadowApplication.getInstance();
+        shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
+        shadowApplication.setSystemService(Context.USER_SERVICE, mUm);
+        mContext = RuntimeEnvironment.application;
+        mController = spy(new InvalidConversationPreferenceController(mContext, mBackend));
+    }
+
+    @After
+    public void tearDown() {
+        SettingsShadowResources.reset();
+    }
+
+    @Test
+    public void testNoCrashIfNoOnResume() {
+        mController.isAvailable();
+        mController.updateState(mock(RestrictedSwitchPreference.class));
+        mController.onPreferenceChange(mock(RestrictedSwitchPreference.class), true);
+    }
+
+    @Test
+    public void testIsAvailable_notIfAppBlocked() {
+        when(mBackend.isInInvalidMsgState(anyString(), anyInt())).thenReturn(true);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "hi";
+        appRow.uid = 0;
+        appRow.banned = true;
+        mController.onResume(appRow, null, null, null, null, null);
+        assertFalse(mController.isAvailable());
+    }
+
+    @Test
+    public void testIsAvailable_notIfInValidMsgState() {
+        when(mBackend.isInInvalidMsgState(anyString(), anyInt())).thenReturn(false);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "hi";
+        appRow.uid = 0;
+        mController.onResume(appRow, null, null, null, null, null);
+        assertFalse(mController.isAvailable());
+    }
+
+    @Test
+    public void testIsAvailable() {
+        when(mBackend.isInInvalidMsgState(anyString(), anyInt())).thenReturn(true);
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "hi";
+        appRow.uid = 0;
+        mController.onResume(appRow, null, null, null, null, null);
+        assertTrue(mController.isAvailable());
+    }
+
+    @Test
+    public void testUpdateState_disabledByAdmin() {
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "hi";
+        appRow.uid = 0;
+        mController.onResume(appRow, null, null,
+                null, null, mock(RestrictedLockUtils.EnforcedAdmin.class));
+
+        Preference pref = new RestrictedSwitchPreference(mContext);
+        mController.updateState(pref);
+
+        assertFalse(pref.isEnabled());
+    }
+
+    @Test
+    public void testUpdateState_notChecked() {
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "pkg";
+        mController.onResume(appRow, null, null, null, null, null);
+
+        when(mBackend.hasUserDemotedInvalidMsgApp(anyString(), anyInt())).thenReturn(false);
+
+        RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext);
+        mController.updateState(pref);
+        assertTrue(pref.isChecked());
+    }
+
+    @Test
+    public void testUpdateState_checked() {
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "pkg";
+        mController.onResume(appRow, null, null, null, null, null);
+
+        when(mBackend.hasUserDemotedInvalidMsgApp(anyString(), anyInt())).thenReturn(true);
+
+        RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext);
+        mController.updateState(pref);
+        assertFalse(pref.isChecked());
+    }
+
+    @Test
+    public void testOnPreferenceChange_toggleEnabled() {
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "pkg";
+        mController.onResume(appRow, null, null, null, null, null);
+
+        when(mBackend.hasUserDemotedInvalidMsgApp(anyString(), anyInt())).thenReturn(true);
+
+        RestrictedSwitchPreference pref =
+                new RestrictedSwitchPreference(mContext);
+        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(pref);
+        mController.displayPreference(mScreen);
+        mController.updateState(pref);
+
+        mController.onPreferenceChange(pref, true);
+
+        verify(mBackend, times(1)).setInvalidMsgAppDemoted(any(), anyInt(), eq(false));
+    }
+
+    @Test
+    public void testOnPreferenceChange_toggleDisabled() {
+        NotificationBackend.AppRow appRow = new NotificationBackend.AppRow();
+        appRow.pkg = "pkg";
+        mController.onResume(appRow, null, null, null, null, null);
+
+        when(mBackend.hasUserDemotedInvalidMsgApp(anyString(), anyInt())).thenReturn(false);
+
+        RestrictedSwitchPreference pref =
+                new RestrictedSwitchPreference(mContext);
+        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(pref);
+        mController.displayPreference(mScreen);
+        mController.updateState(pref);
+
+        mController.onPreferenceChange(pref, false);
+
+        verify(mBackend, times(1)).setInvalidMsgAppDemoted(any(), anyInt(), eq(true));
+    }
+}