diff --git a/res/values/strings.xml b/res/values/strings.xml
index 897cfea93f2..c412f0e6bc4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8385,6 +8385,12 @@
Conversation
+
+ Conversation section
+
+
+ Allow %1$s to appear in the conversation section
+
"%1$s • %2$s"
diff --git a/res/xml/app_notification_settings.xml b/res/xml/app_notification_settings.xml
index f0200ce46b5..8ca4e0ddda1 100644
--- a/res/xml/app_notification_settings.xml
+++ b/res/xml/app_notification_settings.xml
@@ -29,13 +29,6 @@
-
-
-
+ settings:allowDividerBelow="false">
+
+
+
+
+
+
+
diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java
index 6172268bd0b..83df3239d39 100644
--- a/src/com/android/settings/notification/NotificationBackend.java
+++ b/src/com/android/settings/notification/NotificationBackend.java
@@ -268,15 +268,32 @@ public class NotificationBackend {
}
}
- 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 2fcd2b2059a..0ba943605a5 100644
--- a/src/com/android/settings/notification/app/AppConversationListPreferenceController.java
+++ b/src/com/android/settings/notification/app/AppConversationListPreferenceController.java
@@ -46,7 +46,7 @@ public class AppConversationListPreferenceController extends NotificationPrefere
protected List 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 @@ public class AppConversationListPreferenceController extends NotificationPrefere
new AsyncTask() {
@Override
protected Void doInBackground(Void... unused) {
- mHasSentMsg = mBackend.hasSentMessage(mAppRow.pkg, mAppRow.uid);
+ mIsInInvalidMsgState = mBackend.isInInvalidMsgState(mAppRow.pkg, mAppRow.uid);
ParceledListSlice list =
mBackend.getConversations(mAppRow.pkg, mAppRow.uid);
if (list != null) {
@@ -121,20 +121,10 @@ public class AppConversationListPreferenceController extends NotificationPrefere
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 a4228411a72..58c18bae43a 100644
--- a/src/com/android/settings/notification/app/AppNotificationSettings.java
+++ b/src/com/android/settings/notification/app/AppNotificationSettings.java
@@ -122,6 +122,8 @@ public class AppNotificationSettings extends NotificationSettings {
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 0ca2095d6ec..1aed15609ca 100644
--- a/src/com/android/settings/notification/app/BubblePreferenceController.java
+++ b/src/com/android/settings/notification/app/BubblePreferenceController.java
@@ -86,7 +86,7 @@ public class BubblePreferenceController extends NotificationPreferenceController
@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 00000000000..ade9653a5fc
--- /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 00000000000..74f5773efda
--- /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/app/InvalidConversationInfoPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/app/InvalidConversationInfoPreferenceControllerTest.java
new file mode 100644
index 00000000000..aa87539e7c6
--- /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 00000000000..4bfc1b4eb7a
--- /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));
+ }
+}