Create settings screen for Notification Assistant

Test: this atest
Test: manual: change assistant and "adb shell dumpsys notification"
Test: manual: verify persistance through reboot (including none)

Fixes:120852765
Change-Id: Ie4516c3339246d66d7b6719ac5dd1d65c4d03b57
This commit is contained in:
Fabian Kozynski
2019-02-20 12:55:10 -05:00
parent 569aa2594e
commit 01b2a635e9
11 changed files with 447 additions and 0 deletions

View File

@@ -2334,6 +2334,18 @@
android:value="com.android.settings.notification.NotificationAccessSettings" />
</activity>
<activity
android:name="Settings$NotificationAssistantSettingsActivity"
android:label="@string/notification_assistant_title"
android:parentActivityName="Settings">
<intent-filter android:priority="1">
<action android:name="android.settings.NOTIFICATION_ASSISTANT_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.notification.NotificationAssistantPicker" />
</activity>
<activity
android:name="Settings$VrListenersSettingsActivity"
android:label="@string/vr_listeners_title"

View File

@@ -7796,9 +7796,28 @@
<item quantity="other">%d apps can read notifications</item>
</plurals>
<!-- Title for Notification Assistant Picker screen [CHAR LIMIT=30]-->
<string name="notification_assistant_title">Notification Assistant</string>
<!-- Label for no NotificationAssistantService [CHAR_LIMIT=NONE] -->
<string name="no_notification_assistant">No assistant</string>
<!-- String to show in the list of notification listeners, when none is installed -->
<string name="no_notification_listeners">No installed apps have requested notification access.</string>
<!-- Title for a warning message about security implications of enabling a notification
assistant, displayed as a dialog message. [CHAR LIMIT=NONE] -->
<string name="notification_assistant_security_warning_title">Allow notification access for
<xliff:g id="service" example="NotificationAssistant">%1$s</xliff:g>?</string>
<!-- Summary for a warning message about security implications of enabling a notification
listener, displayed as a dialog message. [CHAR LIMIT=NONE] -->
<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,
including personal information such as contact names and the text of messages you receive.
It will also be able to modify or dismiss notifications or trigger action buttons they contain.
\n\nThis will also give the app the ability to turn Do Not Disturb on or off and change related settings.
</string>
<!-- Title for a warning message about security implications of enabling a notification
listener, displayed as a dialog message. [CHAR LIMIT=NONE] -->
<string name="notification_listener_security_warning_title">Allow notification access for

View File

@@ -19,6 +19,13 @@
android:title="@string/configure_notification_settings"
android:key="configure_notification_settings">
<com.android.settingslib.widget.apppreference.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"/>
<SwitchPreference
android:key="hide_silent_icons"
android:title="@string/hide_silent_icons_title"

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/notification_assistant_title" />

View File

@@ -110,6 +110,7 @@ public class Settings extends SettingsActivity {
public static class ZenModeEventRuleSettingsActivity extends SettingsActivity { /* empty */ }
public static class SoundSettingsActivity extends SettingsActivity { /* empty */ }
public static class ConfigureNotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationAssistantSettingsActivity extends SettingsActivity{ /* empty */ }
public static class NotificationAppListActivity extends SettingsActivity { /* empty */ }
public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class ChannelNotificationSettingsActivity extends SettingsActivity { /* empty */ }

View File

@@ -101,6 +101,7 @@ import com.android.settings.notification.ChannelGroupNotificationSettings;
import com.android.settings.notification.ChannelNotificationSettings;
import com.android.settings.notification.ConfigureNotificationSettings;
import com.android.settings.notification.NotificationAccessSettings;
import com.android.settings.notification.NotificationAssistantPicker;
import com.android.settings.notification.NotificationStation;
import com.android.settings.notification.SoundSettings;
import com.android.settings.notification.ZenAccessSettings;
@@ -218,6 +219,7 @@ public class SettingsGateway {
AppInfoDashboardFragment.class.getName(),
BatterySaverSettings.class.getName(),
AppNotificationSettings.class.getName(),
NotificationAssistantPicker.class.getName(),
ChannelNotificationSettings.class.getName(),
ChannelGroupNotificationSettings.class.getName(),
ApnSettings.class.getName(),

View File

@@ -58,6 +58,8 @@ public class ConfigureNotificationSettings extends DashboardFragment implements
static final String KEY_LOCKSCREEN_WORK_PROFILE = "lock_screen_notifications_profile";
@VisibleForTesting
static final String KEY_SWIPE_DOWN = "gesture_swipe_down_fingerprint_notifications";
@VisibleForTesting
static final String KEY_NOTIFICATION_ASSISTANT = "notification_assistant";
private static final String KEY_NOTI_DEFAULT_RINGTONE = "notification_default_ringtone";

View File

@@ -0,0 +1,162 @@
/*
* Copyright (C) 2019 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.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageItemInfo;
import android.content.pm.ServiceInfo;
import android.graphics.drawable.Drawable;
import android.provider.SearchIndexableResource;
import android.provider.Settings;
import android.service.notification.NotificationAssistantService;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.applications.defaultapps.DefaultAppPickerFragment;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.search.Indexable;
import com.android.settingslib.applications.DefaultAppInfo;
import com.android.settingslib.applications.ServiceListing;
import com.android.settingslib.widget.CandidateInfo;
import java.util.ArrayList;
import java.util.List;
public class NotificationAssistantPicker extends DefaultAppPickerFragment implements
ServiceListing.Callback {
private static final String TAG = "NotiAssistantPicker";
@VisibleForTesting
protected NotificationBackend mNotificationBackend;
private List<CandidateInfo> mCandidateInfos = new ArrayList<>();
@VisibleForTesting
protected Context mContext;
private ServiceListing mServiceListing;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mContext = context;
mNotificationBackend = new NotificationBackend();
mServiceListing = new ServiceListing.Builder(context)
.setTag(TAG)
.setSetting(Settings.Secure.ENABLED_NOTIFICATION_ASSISTANT)
.setIntentAction(NotificationAssistantService.SERVICE_INTERFACE)
.setPermission(android.Manifest.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE)
.setNoun("notification assistant")
.build();
mServiceListing.addCallback(this);
mServiceListing.reload();
}
@Override
public void onDetach() {
super.onDetach();
mServiceListing.removeCallback(this);
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.notification_assistant_settings;
}
@Override
protected List<? extends CandidateInfo> getCandidates() {
return mCandidateInfos;
}
@Override
protected String getDefaultKey() {
ComponentName cn = mNotificationBackend.getAllowedNotificationAssistant();
return (cn != null) ? cn.flattenToString() : "";
}
@Override
protected boolean setDefaultKey(String key) {
return mNotificationBackend.setNotificationAssistantGranted(
ComponentName.unflattenFromString(key));
}
@Override
public int getMetricsCategory() {
return SettingsEnums.DEFAULT_NOTIFICATION_ASSISTANT;
}
@Override
protected CharSequence getConfirmationMessage(CandidateInfo info) {
if (TextUtils.isEmpty(info.getKey())) {
return null;
}
return mContext.getString(R.string.notification_assistant_security_warning_summary,
info.loadLabel());
}
@Override
public void onServicesReloaded(List<ServiceInfo> services) {
List<CandidateInfo> list = new ArrayList<>();
services.sort(new PackageItemInfo.DisplayNameComparator(mPm));
for (ServiceInfo service : services) {
final ComponentName cn = new ComponentName(service.packageName, service.name);
list.add(new DefaultAppInfo(mContext, mPm, mUserId, cn));
}
list.add(new CandidateNone(mContext));
mCandidateInfos = list;
}
public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider() {
@Override
public List<SearchIndexableResource> getXmlResourcesToIndex(Context context,
boolean enabled) {
final List<SearchIndexableResource> result = new ArrayList<>();
final SearchIndexableResource sir = new SearchIndexableResource(context);
sir.xmlResId = R.xml.notification_assistant_settings;
result.add(sir);
return result;
}
};
public static class CandidateNone extends CandidateInfo {
public Context mContext;
public CandidateNone(Context context) {
super(true);
mContext = context;
}
@Override
public CharSequence loadLabel() {
return mContext.getString(R.string.no_notification_assistant);
}
@Override
public Drawable loadIcon() {
return null;
}
@Override
public String getKey() {
return "";
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 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.core.BasePreferenceController;
public class NotificationAssistantPreferenceController extends BasePreferenceController {
public NotificationAssistantPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
@Override
public int getAvailabilityStatus() {
return BasePreferenceController.AVAILABLE;
}
}

View File

@@ -23,6 +23,7 @@ import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -410,6 +411,29 @@ public class NotificationBackend {
}
}
public ComponentName getAllowedNotificationAssistant() {
try {
return sINM.getAllowedNotificationAssistant();
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return null;
}
}
public boolean setNotificationAssistantGranted(ComponentName cn) {
try {
sINM.setNotificationAssistantAccessGranted(cn, true);
if (cn == null) {
return sINM.getAllowedNotificationAssistant() == null;
} else {
return cn.equals(sINM.getAllowedNotificationAssistant());
}
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
/**
* NotificationsSentState contains how often an app sends notifications and how recently it sent
* one.

View File

@@ -0,0 +1,165 @@
/*
* Copyright (C) 2019 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.junit.Assert.fail;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_SMART_NULLS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import com.android.settingslib.widget.CandidateInfo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
import org.mockito.invocation.InvocationOnMock;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class NotificationAssistantPickerTest {
private NotificationAssistantPicker mFragment;
@Mock
private Context mContext;
@Mock
private PackageManager mPackageManager;
@Mock
private NotificationBackend mNotificationBackend;
private static final String TEST_PKG = "test.package";
private static final String TEST_SRV = "test.component";
private static final String TEST_CMP = TEST_PKG + "/" + TEST_SRV;
private static final String TEST_NAME = "Test name";
private static final ComponentName TEST_COMPONENT = ComponentName.unflattenFromString(TEST_CMP);
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mFragment = new TestNotificationAssistantPicker(mContext, mPackageManager,
mNotificationBackend);
}
@Test
public void getCurrentAssistant() {
when(mNotificationBackend.getAllowedNotificationAssistant()).thenReturn(TEST_COMPONENT);
String key = mFragment.getDefaultKey();
assertEquals(key, TEST_CMP);
}
@Test
public void getCurrentAssistant_None() {
when(mNotificationBackend.getAllowedNotificationAssistant()).thenReturn(null);
String key = mFragment.getDefaultKey();
assertEquals(key, "");
}
@Test
public void setAssistant() {
mFragment.setDefaultKey(TEST_CMP);
verify(mNotificationBackend).setNotificationAssistantGranted(TEST_COMPONENT);
}
@Test
public void setAssistant_None() {
mFragment.setDefaultKey("");
verify(mNotificationBackend).setNotificationAssistantGranted(null);
}
@Test
public void candidateListHasNoneAtEnd() {
List<ServiceInfo> list = new ArrayList<>();
ServiceInfo serviceInfo = mock(ServiceInfo.class, RETURNS_SMART_NULLS);
serviceInfo.packageName = TEST_PKG;
serviceInfo.name = TEST_SRV;
list.add(serviceInfo);
mFragment.onServicesReloaded(list);
List<? extends CandidateInfo> candidates = mFragment.getCandidates();
assertTrue(candidates.size() > 0);
assertEquals(candidates.get(candidates.size() - 1).getKey(), "");
}
@Test
public void candidateListHasCorrectCandidate() {
List<ServiceInfo> list = new ArrayList<>();
ServiceInfo serviceInfo = mock(ServiceInfo.class, RETURNS_SMART_NULLS);
serviceInfo.packageName = TEST_PKG;
serviceInfo.name = TEST_SRV;
list.add(serviceInfo);
mFragment.onServicesReloaded(list);
List<? extends CandidateInfo> candidates = mFragment.getCandidates();
boolean found = false;
for (CandidateInfo c : candidates) {
if (TEST_CMP.equals(c.getKey())) {
found = true;
break;
}
}
if (!found) fail();
}
@Test
public void noDialogOnNoAssistantSelected() {
when(mContext.getString(anyInt(), anyString())).thenAnswer(
(InvocationOnMock invocation) -> {
return invocation.getArgument(1);
});
assertNull(mFragment.getConfirmationMessage(
new NotificationAssistantPicker.CandidateNone(mContext)));
}
@Test
public void dialogTextHasAssistantName() {
CandidateInfo c = mock(CandidateInfo.class);
when(mContext.getString(anyInt(), anyString())).thenAnswer(
(InvocationOnMock invocation) -> {
return invocation.getArgument(1);
});
when(c.loadLabel()).thenReturn(TEST_NAME);
when(c.getKey()).thenReturn(TEST_CMP);
CharSequence text = mFragment.getConfirmationMessage(c);
assertNotNull(text);
assertTrue(text.toString().contains(TEST_NAME));
}
private static class TestNotificationAssistantPicker extends NotificationAssistantPicker {
TestNotificationAssistantPicker(Context context, PackageManager packageManager,
NotificationBackend notificationBackend) {
mContext = context;
mPm = packageManager;
mNotificationBackend = notificationBackend;
}
}
}