NAS Setting Migration

Change NAS setting to a toggle setting and move the settings to
Notifications/General

Bug: 173106358
Test: tested manually on device, make RunSettingsRoboTests
Change-Id: I1ba1214511dceea6faf5fb39692d920e761b33d8
This commit is contained in:
Chloris Kuo
2021-03-19 07:00:45 -07:00
parent 473fffa318
commit 2c955bbae2
11 changed files with 315 additions and 75 deletions

View File

@@ -2631,7 +2631,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS" <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.notification.NotificationAssistantPicker" /> android:value="com.android.settings.notification.ConfigureNotificationSettings" />
</activity> </activity>
<activity <activity

View File

@@ -9032,8 +9032,10 @@
<item quantity="other">%d apps can read notifications</item> <item quantity="other">%d apps can read notifications</item>
</plurals> </plurals>
<!-- Title for Notification Assistant Picker screen [CHAR LIMIT=30]--> <!-- Title for Notification Assistant setting [CHAR LIMIT=30]-->
<string name="notification_assistant_title">Adaptive Notifications</string> <string name="notification_assistant_title">Enhanced notifications</string>
<!-- Summary of Notification Assistant provided features [CHAR LIMIT=NONE]-->
<string name="notification_assistant_summary">Get suggested actions, replies, and more</string>
<!-- Label for no NotificationAssistantService [CHAR_LIMIT=NONE] --> <!-- Label for no NotificationAssistantService [CHAR_LIMIT=NONE] -->
<string name="no_notification_assistant">None</string> <string name="no_notification_assistant">None</string>
@@ -9051,10 +9053,11 @@
<!-- Summary for a warning message about security implications of enabling a notification <!-- Summary for a warning message about security implications of enabling a notification
listener, displayed as a dialog message. [CHAR LIMIT=NONE] --> listener, displayed as a dialog message. [CHAR LIMIT=NONE] -->
<string name="notification_assistant_security_warning_summary"> <string name="notification_assistant_security_warning_summary">
<xliff:g id="notification_assistant_name" example="Notification Assistant">%1$s</xliff:g> will be able to read all notifications, Enhanced notifications can read all notification content,
including personal information such as contact names and the text of messages you receive. including personal information like contact names and messages.
This app will also be able to dismiss notifications or take action on buttons in notifications, including answering phone calls. This feature can also dismiss notifications or take actions on buttons in notifications,
\n\nThis will also give the app the ability to turn Do Not Disturb on or off and change related settings. such as answering phone calls.
\n\nThis feature can also turn Priority mode on or off and change related settings.
</string> </string>
<!-- Title for a warning message about security implications of enabling a notification <!-- Title for a warning message about security implications of enabling a notification

View File

@@ -115,6 +115,11 @@
android:title="@string/snooze_options_title" android:title="@string/snooze_options_title"
settings:controller="com.android.settings.notification.SnoozeNotificationPreferenceController" /> settings:controller="com.android.settings.notification.SnoozeNotificationPreferenceController" />
<SwitchPreference
android:key="notification_assistant"
android:title="@string/notification_assistant_title"
android:summary="@string/notification_assistant_summary"/>
<!-- Notification badging --> <!-- Notification badging -->
<SwitchPreference <SwitchPreference
android:key="notification_badging" android:key="notification_badging"

View File

@@ -155,5 +155,11 @@
android:order="22" android:order="22"
android:title="@string/notification_pulse_title" android:title="@string/notification_pulse_title"
settings:controller="com.android.settings.notification.PulseNotificationPreferenceController"/> settings:controller="com.android.settings.notification.PulseNotificationPreferenceController"/>
<SwitchPreference
android:key="notification_assistant"
android:order="23"
android:title="@string/notification_assistant_title"
android:summary="@string/notification_assistant_summary"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@@ -80,13 +80,6 @@
android:value="com.android.settings.Settings$WriteSettingsActivity" /> android:value="com.android.settings.Settings$WriteSettingsActivity" />
</Preference> </Preference>
<com.android.settingslib.widget.AppPreference
android:key="notification_assistant"
android:title="@string/notification_assistant_title"
android:summary="@string/summary_placeholder"
settings:fragment="com.android.settings.notification.NotificationAssistantPicker"
settings:controller="com.android.settings.notification.NotificationAssistantPreferenceController"/>
<Preference <Preference
android:key="notification_access" android:key="notification_access"
android:title="@string/manage_notification_access_title" android:title="@string/manage_notification_access_title"

View File

@@ -22,6 +22,7 @@ import android.app.Activity;
import android.app.Application; import android.app.Application;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
import android.app.usage.IUsageStatsManager; import android.app.usage.IUsageStatsManager;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
@@ -62,6 +63,7 @@ public class ConfigureNotificationSettings extends DashboardFragment implements
private static final int REQUEST_CODE = 200; private static final int REQUEST_CODE = 200;
private static final String SELECTED_PREFERENCE_KEY = "selected_preference"; private static final String SELECTED_PREFERENCE_KEY = "selected_preference";
private static final String KEY_ADVANCED_CATEGORY = "configure_notifications_advanced"; private static final String KEY_ADVANCED_CATEGORY = "configure_notifications_advanced";
private static final String KEY_NAS = "notification_assistant";
private RingtonePreference mRequestPreference; private RingtonePreference mRequestPreference;
@@ -116,6 +118,8 @@ public class ConfigureNotificationSettings extends DashboardFragment implements
} }
}); });
controllers.add(new NotificationAssistantPreferenceController(context,
new NotificationBackend(), host, KEY_NAS));
if (FeatureFlagUtils.isEnabled(context, FeatureFlags.SILKY_HOME)) { if (FeatureFlagUtils.isEnabled(context, FeatureFlags.SILKY_HOME)) {
controllers.add(new EmergencyBroadcastPreferenceController(context, controllers.add(new EmergencyBroadcastPreferenceController(context,
@@ -199,4 +203,14 @@ public class ConfigureNotificationSettings extends DashboardFragment implements
return keys; return keys;
} }
}; };
// Dialogs only have access to the parent fragment, not the controller, so pass the information
// along to keep business logic out of this file
protected void enableNAS(ComponentName cn) {
final PreferenceScreen screen = getPreferenceScreen();
NotificationAssistantPreferenceController napc =
use(NotificationAssistantPreferenceController.class);
napc.setNotificationAssistantGranted(cn);
napc.updateState(screen.findPreference(napc.getPreferenceKey()));
}
} }

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
public class NotificationAssistantDialogFragment extends InstrumentedDialogFragment
implements DialogInterface.OnClickListener {
static final String KEY_COMPONENT = "c";
public static NotificationAssistantDialogFragment newInstance(Fragment target,
ComponentName cn) {
final NotificationAssistantDialogFragment dialogFragment =
new NotificationAssistantDialogFragment();
final Bundle args = new Bundle();
args.putString(KEY_COMPONENT, cn.flattenToString());
dialogFragment.setArguments(args);
dialogFragment.setTargetFragment(target, 0);
return dialogFragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final String summary = getResources()
.getString(R.string.notification_assistant_security_warning_summary);
return new AlertDialog.Builder(getContext())
.setMessage(summary)
.setCancelable(true)
.setPositiveButton(R.string.okay, this)
.create();
}
@Override
public int getMetricsCategory() {
return SettingsEnums.DEFAULT_NOTIFICATION_ASSISTANT;
}
@Override
public void onClick(DialogInterface dialog, int which) {
final Bundle args = getArguments();
final ComponentName cn = ComponentName.unflattenFromString(args
.getString(KEY_COMPONENT));
ConfigureNotificationSettings parent = (ConfigureNotificationSettings) getTargetFragment();
parent.enableNAS(cn);
}
}

View File

@@ -18,44 +18,72 @@ package com.android.settings.notification;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle; import android.os.UserHandle;
import android.provider.Settings;
import com.android.settings.core.BasePreferenceController; import androidx.fragment.app.Fragment;
import com.android.settingslib.applications.DefaultAppInfo;
import com.android.settingslib.widget.CandidateInfo; import com.android.settings.core.TogglePreferenceController;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
public class NotificationAssistantPreferenceController extends BasePreferenceController { public class NotificationAssistantPreferenceController extends TogglePreferenceController {
private static final String TAG = "NASPreferenceController";
private static final int AVAILABLE = 1;
private Fragment mFragment;
private int mUserId = UserHandle.myUserId();
@VisibleForTesting @VisibleForTesting
protected NotificationBackend mNotificationBackend; protected NotificationBackend mNotificationBackend;
private PackageManager mPackageManager;
public NotificationAssistantPreferenceController(Context context, String preferenceKey) { public NotificationAssistantPreferenceController(Context context, NotificationBackend backend,
Fragment fragment, String preferenceKey) {
super(context, preferenceKey); super(context, preferenceKey);
mNotificationBackend = new NotificationBackend(); mNotificationBackend = backend;
mPackageManager = mContext.getPackageManager(); mFragment = fragment;
} }
@Override @Override
public int getAvailabilityStatus() { public int getAvailabilityStatus() {
return BasePreferenceController.AVAILABLE; return AVAILABLE;
} }
@Override @Override
public CharSequence getSummary() { public boolean isChecked() {
CandidateInfo appSelected = new NotificationAssistantPicker.CandidateNone(mContext); ComponentName acn = mNotificationBackend.getAllowedNotificationAssistant();
ComponentName assistant = mNotificationBackend.getAllowedNotificationAssistant(); ComponentName dcn = mNotificationBackend.getDefaultNotificationAssistant();
if (assistant != null) { return (acn != null && acn.equals(dcn));
appSelected = createCandidateInfo(assistant);
}
return appSelected.loadLabel();
} }
@VisibleForTesting @Override
protected CandidateInfo createCandidateInfo(ComponentName cn) { public boolean setChecked(boolean isChecked) {
return new DefaultAppInfo(mContext, mPackageManager, UserHandle.myUserId(), cn); ComponentName cn = isChecked
? mNotificationBackend.getDefaultNotificationAssistant() : null;
if (isChecked) {
if (mFragment == null) {
throw new IllegalStateException("No fragment to start activity");
}
showDialog(cn);
return false;
} else {
setNotificationAssistantGranted(null);
return true;
}
} }
}
protected void setNotificationAssistantGranted(ComponentName cn) {
if (Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.NAS_SETTINGS_UPDATED, 0, mUserId) == 0) {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.NAS_SETTINGS_UPDATED, 1, mUserId);
mNotificationBackend.resetDefaultNotificationAssistant(cn != null);
}
mNotificationBackend.setNotificationAssistantGranted(cn);
}
protected void showDialog(ComponentName cn) {
NotificationAssistantDialogFragment dialogFragment =
NotificationAssistantDialogFragment.newInstance(mFragment, cn);
dialogFragment.show(mFragment.getFragmentManager(), TAG);
}
}

View File

@@ -19,7 +19,6 @@ import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER; import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
import android.app.INotificationManager; import android.app.INotificationManager;
@@ -50,7 +49,6 @@ import android.service.notification.NotificationListenerFilter;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.util.IconDrawableFactory; import android.util.IconDrawableFactory;
import android.util.Log; import android.util.Log;
import android.util.Slog;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@@ -563,6 +561,23 @@ public class NotificationBackend {
} }
} }
public ComponentName getDefaultNotificationAssistant() {
try {
return sINM.getDefaultNotificationAssistant();
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return null;
}
}
public void resetDefaultNotificationAssistant(boolean loadFromConfig) {
try {
sINM.resetDefaultNotificationAssistant(loadFromConfig);
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
}
}
public boolean setNotificationAssistantGranted(ComponentName cn) { public boolean setNotificationAssistantGranted(ComponentName cn) {
try { try {
sINM.setNotificationAssistantAccessGranted(cn, true); sINM.setNotificationAssistantAccessGranted(cn, true);

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import androidx.fragment.app.FragmentActivity;
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;
@RunWith(RobolectricTestRunner.class)
public class NotificationAssistantDialogFragmentTest {
private Context mContext;
@Mock
private ConfigureNotificationSettings mFragment;
private NotificationAssistantDialogFragment mDialogFragment;
@Mock
private FragmentActivity mActivity;
ComponentName mComponentName = new ComponentName("a", "b");
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
mDialogFragment =
spy(NotificationAssistantDialogFragment.newInstance(mFragment, mComponentName));
doReturn(mActivity).when(mDialogFragment).getActivity();
doReturn(mContext).when(mDialogFragment).getContext();
}
@Test
public void testClickOK_callEnableNAS() {
mDialogFragment.onClick(null, DialogInterface.BUTTON_POSITIVE);
verify(mFragment, times(1)).enableNAS(eq(mComponentName));
}
}

View File

@@ -16,17 +16,25 @@
package com.android.settings.notification; package com.android.settings.notification;
import static junit.framework.TestCase.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
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 static org.mockito.Mockito.when;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.provider.Settings;
import android.graphics.drawable.Drawable;
import android.os.Debug;
import com.android.settingslib.widget.CandidateInfo; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -35,7 +43,6 @@ import org.mockito.Answers;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class NotificationAssistantPreferenceControllerTest { public class NotificationAssistantPreferenceControllerTest {
@@ -44,57 +51,86 @@ public class NotificationAssistantPreferenceControllerTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext; private Context mContext;
@Mock @Mock
private ConfigureNotificationSettings mFragment;
@Mock
private FragmentManager mFragmentManager;
@Mock
private FragmentTransaction mFragmentTransaction;
@Mock
private NotificationBackend mBackend; private NotificationBackend mBackend;
private NotificationAssistantPreferenceController mPreferenceController; private NotificationAssistantPreferenceController mPreferenceController;
ComponentName mNASComponent = new ComponentName("a", "b");
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application; mContext = spy(ApplicationProvider.getApplicationContext());
mPreferenceController = new TestPreferenceController(mContext, mBackend); doReturn(mContext).when(mFragment).getContext();
when(mFragment.getFragmentManager()).thenReturn(mFragmentManager);
when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
when(mBackend.getDefaultNotificationAssistant()).thenReturn(mNASComponent);
mPreferenceController = new NotificationAssistantPreferenceController(mContext,
mBackend, mFragment, KEY);
} }
@Test @Test
public void testGetSummary_noAssistant() { public void testIsChecked() throws Exception {
when(mBackend.getAllowedNotificationAssistant()).thenReturn(mNASComponent);
assertTrue(mPreferenceController.isChecked());
when(mBackend.getAllowedNotificationAssistant()).thenReturn(null); when(mBackend.getAllowedNotificationAssistant()).thenReturn(null);
CharSequence noneLabel = new NotificationAssistantPicker.CandidateNone(mContext) assertFalse(mPreferenceController.isChecked());
.loadLabel();
assertEquals(noneLabel, mPreferenceController.getSummary());
} }
@Test @Test
public void testGetSummary_TestAssistant() { public void testSetChecked() throws Exception {
String testName = "test_pkg/test_cls"; // Verify a dialog is shown when the switch is to be enabled.
when(mBackend.getAllowedNotificationAssistant()).thenReturn( assertFalse(mPreferenceController.setChecked(true));
ComponentName.unflattenFromString(testName)); verify(mFragmentTransaction).add(
assertEquals(testName, mPreferenceController.getSummary()); any(NotificationAssistantDialogFragment.class), anyString());
verify(mBackend, times(0)).setNotificationAssistantGranted(any());
// Verify no dialog is shown and NAS set to null when disabled
assertTrue(mPreferenceController.setChecked(false));
verify(mBackend, times(1)).setNotificationAssistantGranted(null);
} }
private final class TestPreferenceController extends NotificationAssistantPreferenceController { @Test
public void testMigrationFromSetting_userEnable() throws Exception {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.NAS_SETTINGS_UPDATED, 0, 0);
private TestPreferenceController(Context context, NotificationBackend backend) { //Test user enable for the first time
super(context, KEY); mPreferenceController.setNotificationAssistantGranted(mNASComponent);
mNotificationBackend = backend; assertEquals(1, Settings.Secure.getIntForUser(mContext.getContentResolver(),
} Settings.Secure.NAS_SETTINGS_UPDATED, 0, 0));
verify(mBackend, times(1))
.resetDefaultNotificationAssistant(eq(true));
@Override //Test user enable again, migration should not happen
public String getPreferenceKey() { mPreferenceController.setNotificationAssistantGranted(mNASComponent);
return KEY; //Number of invocations should not increase
} verify(mBackend, times(1))
.resetDefaultNotificationAssistant(eq(true));
}
@Override @Test
protected CandidateInfo createCandidateInfo(ComponentName cn) { public void testMigrationFromSetting_userDisable() throws Exception {
return new CandidateInfo(true) { Settings.Secure.putIntForUser(mContext.getContentResolver(),
@Override Settings.Secure.NAS_SETTINGS_UPDATED, 0, 0);
public CharSequence loadLabel() { return cn.flattenToString(); }
@Override //Test user disable for the first time
public Drawable loadIcon() { return null; } mPreferenceController.setChecked(false);
assertEquals(1, Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.NAS_SETTINGS_UPDATED, 0, 0));
verify(mBackend, times(1))
.resetDefaultNotificationAssistant(eq(false));
@Override //Test user disable again, migration should not happen
public String getKey() { return null; } mPreferenceController.setChecked(false);
}; //Number of invocations should not increase
} verify(mBackend, times(1))
.resetDefaultNotificationAssistant(eq(false));
} }
} }