Merge "Add knobs for tweaking blocking helper thresholds" into pi-dev

This commit is contained in:
Rohan Shah
2018-05-08 21:00:07 +00:00
committed by Android (Google) Code Review
8 changed files with 253 additions and 28 deletions

View File

@@ -12429,7 +12429,7 @@ public final class Settings {
*/
public static final String MULTI_SIM_SMS_SUBSCRIPTION = "multi_sim_sms";
/**
/**
* Used to provide option to user to select subscription during send SMS.
* The value 1 - enable, 0 - disable
* @hide
@@ -12570,6 +12570,28 @@ public final class Settings {
public static final String NOTIFICATION_SNOOZE_OPTIONS =
"notification_snooze_options";
/**
* Settings key for the ratio of notification dismissals to notification views - one of the
* criteria for showing the notification blocking helper.
*
* <p>The value is a float ranging from 0.0 to 1.0 (the closer to 0.0, the more intrusive
* the blocking helper will be).
*
* @hide
*/
public static final String BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT =
"blocking_helper_dismiss_to_view_ratio";
/**
* Settings key for the longest streak of dismissals - one of the criteria for showing the
* notification blocking helper.
*
* <p>The value is an integer greater than 0.
*
* @hide
*/
public static final String BLOCKING_HELPER_STREAK_LIMIT = "blocking_helper_streak_limit";
/**
* Configuration flags for SQLite Compatibility WAL. Encoded as a key-value list, separated
* by commas. E.g.: compatibility_wal_supported=true, wal_syncmode=OFF

View File

@@ -48,7 +48,10 @@ public abstract class NotificationAssistantService extends NotificationListenerS
public static final String SERVICE_INTERFACE
= "android.service.notification.NotificationAssistantService";
private Handler mHandler;
/**
* @hide
*/
protected Handler mHandler;
@Override
protected void attachBaseContext(Context base) {

View File

@@ -131,6 +131,8 @@ public class SettingsBackupTest {
Settings.Global.BLE_SCAN_LOW_LATENCY_WINDOW_MS,
Settings.Global.BLE_SCAN_LOW_LATENCY_INTERVAL_MS,
Settings.Global.BLE_SCAN_BACKGROUND_MODE,
Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
Settings.Global.BLOCKING_HELPER_STREAK_LIMIT,
Settings.Global.BLUETOOTH_A2DP_SINK_PRIORITY_PREFIX,
Settings.Global.BLUETOOTH_A2DP_SRC_PRIORITY_PREFIX,
Settings.Global.BLUETOOTH_A2DP_SUPPORTS_OPTIONAL_CODECS_PREFIX,

View File

@@ -17,16 +17,20 @@
package android.ext.services.notification;
import static android.app.NotificationManager.IMPORTANCE_MIN;
import static android.service.notification.NotificationListenerService.Ranking
.USER_SENTIMENT_NEGATIVE;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
import android.app.INotificationManager;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.ext.services.R;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.storage.StorageManager;
import android.provider.Settings;
import android.service.notification.Adjustment;
import android.service.notification.NotificationAssistantService;
import android.service.notification.NotificationStats;
@@ -74,9 +78,12 @@ public class Assistant extends NotificationAssistantService {
PREJUDICAL_DISMISSALS.add(REASON_LISTENER_CANCEL);
}
private float mDismissToViewRatioLimit;
private int mStreakLimit;
// key : impressions tracker
// TODO: prune deleted channels and apps
ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>();
final ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>();
// SBN key : channel id
ArrayMap<String, String> mLiveNotifications = new ArrayMap<>();
@@ -86,6 +93,14 @@ public class Assistant extends NotificationAssistantService {
public Assistant() {
}
@Override
public void onCreate() {
super.onCreate();
// Contexts are correctly hooked up by the creation step, which is required for the observer
// to be hooked up/initialized.
new SettingsObserver(mHandler);
}
private void loadFile() {
if (DEBUG) Slog.d(TAG, "loadFile");
AsyncTask.execute(() -> {
@@ -120,7 +135,7 @@ public class Assistant extends NotificationAssistantService {
continue;
}
String key = parser.getAttributeValue(null, ATT_KEY);
ChannelImpressions ci = new ChannelImpressions();
ChannelImpressions ci = createChannelImpressionsWithThresholds();
ci.populateFromXml(parser);
synchronized (mkeyToImpressions) {
ci.append(mkeyToImpressions.get(key));
@@ -184,7 +199,7 @@ public class Assistant extends NotificationAssistantService {
String key = getKey(
sbn.getPackageName(), sbn.getUserId(), ranking.getChannel().getId());
ChannelImpressions ci = mkeyToImpressions.getOrDefault(key,
new ChannelImpressions());
createChannelImpressionsWithThresholds());
if (ranking.getImportance() > IMPORTANCE_MIN && ci.shouldTriggerBlock()) {
adjustNotification(createNegativeAdjustment(
sbn.getPackageName(), sbn.getKey(), sbn.getUserId()));
@@ -206,7 +221,7 @@ public class Assistant extends NotificationAssistantService {
String key = getKey(sbn.getPackageName(), sbn.getUserId(), channelId);
synchronized (mkeyToImpressions) {
ChannelImpressions ci = mkeyToImpressions.getOrDefault(key,
new ChannelImpressions());
createChannelImpressionsWithThresholds());
if (stats.hasSeen()) {
ci.incrementViews();
updatedImpressions = true;
@@ -310,4 +325,58 @@ public class Assistant extends NotificationAssistantService {
mkeyToImpressions.put(key, ci);
}
}
private ChannelImpressions createChannelImpressionsWithThresholds() {
ChannelImpressions impressions = new ChannelImpressions();
impressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit);
return impressions;
}
/**
* Observer for updates on blocking helper threshold values.
*/
private final class SettingsObserver extends ContentObserver {
private final Uri STREAK_LIMIT_URI =
Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT);
private final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI =
Settings.Global.getUriFor(
Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT);
public SettingsObserver(Handler handler) {
super(handler);
ContentResolver resolver = getApplicationContext().getContentResolver();
resolver.registerContentObserver(
DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, getUserId());
resolver.registerContentObserver(STREAK_LIMIT_URI, false, this, getUserId());
// Update all uris on creation.
update(null);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
update(uri);
}
private void update(Uri uri) {
ContentResolver resolver = getApplicationContext().getContentResolver();
if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) {
mDismissToViewRatioLimit = Settings.Global.getFloat(
resolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT);
}
if (uri == null || STREAK_LIMIT_URI.equals(uri)) {
mStreakLimit = Settings.Global.getInt(
resolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT,
ChannelImpressions.DEFAULT_STREAK_LIMIT);
}
// Update all existing channel impression objects with any new limits/thresholds.
synchronized (mkeyToImpressions) {
for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) {
channelImpressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit);
}
}
}
}
}

View File

@@ -21,6 +21,8 @@ import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
@@ -30,8 +32,8 @@ public final class ChannelImpressions implements Parcelable {
private static final String TAG = "ExtAssistant.CI";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
static final double DISMISS_TO_VIEW_RATIO_LIMIT = .4;
static final int STREAK_LIMIT = 2;
static final float DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT = .4f;
static final int DEFAULT_STREAK_LIMIT = 2;
static final String ATT_DISMISSALS = "dismisses";
static final String ATT_VIEWS = "views";
static final String ATT_STREAK = "streak";
@@ -40,18 +42,20 @@ public final class ChannelImpressions implements Parcelable {
private int mViews = 0;
private int mStreak = 0;
public ChannelImpressions() {
}
private float mDismissToViewRatioLimit;
private int mStreakLimit;
public ChannelImpressions(int dismissals, int views) {
mDismissals = dismissals;
mViews = views;
public ChannelImpressions() {
mDismissToViewRatioLimit = DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT;
mStreakLimit = DEFAULT_STREAK_LIMIT;
}
protected ChannelImpressions(Parcel in) {
mDismissals = in.readInt();
mViews = in.readInt();
mStreak = in.readInt();
mDismissToViewRatioLimit = in.readFloat();
mStreakLimit = in.readInt();
}
public int getStreak() {
@@ -71,6 +75,21 @@ public final class ChannelImpressions implements Parcelable {
mStreak++;
}
void updateThresholds(float dismissToViewRatioLimit, int streakLimit) {
mDismissToViewRatioLimit = dismissToViewRatioLimit;
mStreakLimit = streakLimit;
}
@VisibleForTesting
float getDismissToViewRatioLimit() {
return mDismissToViewRatioLimit;
}
@VisibleForTesting
int getStreakLimit() {
return mStreakLimit;
}
public void append(ChannelImpressions additionalImpressions) {
if (additionalImpressions != null) {
mViews += additionalImpressions.getViews();
@@ -94,8 +113,8 @@ public final class ChannelImpressions implements Parcelable {
if (DEBUG) {
Log.d(TAG, "should trigger? " + getDismissals() + " " + getViews() + " " + getStreak());
}
return ((double) getDismissals() / getViews()) > DISMISS_TO_VIEW_RATIO_LIMIT
&& getStreak() > STREAK_LIMIT;
return ((float) getDismissals() / getViews()) > mDismissToViewRatioLimit
&& getStreak() > mStreakLimit;
}
@Override
@@ -103,6 +122,8 @@ public final class ChannelImpressions implements Parcelable {
dest.writeInt(mDismissals);
dest.writeInt(mViews);
dest.writeInt(mStreak);
dest.writeFloat(mDismissToViewRatioLimit);
dest.writeInt(mStreakLimit);
}
@Override
@@ -148,7 +169,9 @@ public final class ChannelImpressions implements Parcelable {
sb.append("mDismissals=").append(mDismissals);
sb.append(", mViews=").append(mViews);
sb.append(", mStreak=").append(mStreak);
sb.append('}');
sb.append(", thresholds=(").append(mDismissToViewRatioLimit);
sb.append(",").append(mStreakLimit);
sb.append(")}");
return sb.toString();
}

View File

@@ -17,6 +17,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.ext.services.tests.unit">
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<application>
<uses-library android:name="android.test.runner" />
</application>

View File

@@ -20,6 +20,7 @@ import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_MIN;
import static junit.framework.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -27,11 +28,15 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Application;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.content.ContentResolver;
import android.content.IContentProvider;
import android.content.Intent;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.Adjustment;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.Ranking;
@@ -78,10 +83,10 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
new NotificationChannel("one", "", IMPORTANCE_LOW);
@Mock INotificationManager mNoMan;
@Mock
AtomicFile mFile;
@Mock AtomicFile mFile;
Assistant mAssistant;
Application mApplication;
@Rule
public final TestableContext mContext =
@@ -98,6 +103,16 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
Intent startIntent =
new Intent("android.service.notification.NotificationAssistantService");
startIntent.setPackage("android.ext.services");
// To bypass real calls to global settings values, set the Settings values here.
Settings.Global.putFloat(mContext.getContentResolver(),
Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f);
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2);
mApplication = (Application) InstrumentationRegistry.getInstrumentation().
getTargetContext().getApplicationContext();
// Force the test to use the correct application instead of trying to use a mock application
setApplication(mApplication);
bindService(startIntent);
mAssistant = getService();
mAssistant.setNoMan(mNoMan);
@@ -128,7 +143,7 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
}
private void almostBlockChannel(String pkg, int uid, NotificationChannel channel) {
for (int i = 0; i < ChannelImpressions.STREAK_LIMIT; i++) {
for (int i = 0; i < ChannelImpressions.DEFAULT_STREAK_LIMIT; i++) {
dismissBadNotification(pkg, uid, channel, String.valueOf(i));
}
}
@@ -358,7 +373,7 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
@Test
public void testRoundTripXml() throws Exception {
String key1 = mAssistant.getKey("pkg1", 1, "channel1");
ChannelImpressions ci1 = new ChannelImpressions(9, 10);
ChannelImpressions ci1 = new ChannelImpressions();
String key2 = mAssistant.getKey("pkg1", 1, "channel2");
ChannelImpressions ci2 = new ChannelImpressions();
for (int i = 0; i < 3; i++) {
@@ -391,4 +406,43 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
assertEquals(ci3, assistant.getImpressions(key3));
}
@Test
public void testSettingsProviderUpdate() {
ContentResolver resolver = mApplication.getContentResolver();
// Set up channels
String key = mAssistant.getKey("pkg1", 1, "channel1");
ChannelImpressions ci = new ChannelImpressions();
for (int i = 0; i < 3; i++) {
ci.incrementViews();
if (i % 2 == 0) {
ci.incrementDismissals();
}
}
mAssistant.insertImpressions(key, ci);
// With default values, the blocking helper shouldn't be triggered.
assertEquals(false, ci.shouldTriggerBlock());
// Update settings values.
float newDismissToViewRatioLimit = 0f;
int newStreakLimit = 0;
Settings.Global.putFloat(resolver,
Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
newDismissToViewRatioLimit);
Settings.Global.putInt(resolver,
Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit);
// Notify for the settings values we updated.
resolver.notifyChange(
Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT), null);
resolver.notifyChange(
Settings.Global.getUriFor(
Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT),
null);
// With the new threshold, the blocking helper should be triggered.
assertEquals(true, ci.shouldTriggerBlock());
}
}

View File

@@ -16,7 +16,8 @@
package android.ext.services.notification;
import static android.ext.services.notification.ChannelImpressions.STREAK_LIMIT;
import static android.ext.services.notification.ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT;
import static android.ext.services.notification.ChannelImpressions.DEFAULT_STREAK_LIMIT;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@@ -37,7 +38,7 @@ public class ChannelImpressionsTest {
public void testNoStreakNoBlock() {
ChannelImpressions ci = new ChannelImpressions();
for (int i = 0; i < STREAK_LIMIT - 1; i++) {
for (int i = 0; i < DEFAULT_STREAK_LIMIT - 1; i++) {
ci.incrementViews();
ci.incrementDismissals();
}
@@ -49,10 +50,10 @@ public class ChannelImpressionsTest {
public void testNoStreakNoBlock_breakStreak() {
ChannelImpressions ci = new ChannelImpressions();
for (int i = 0; i < STREAK_LIMIT; i++) {
for (int i = 0; i < DEFAULT_STREAK_LIMIT; i++) {
ci.incrementViews();
ci.incrementDismissals();
if (i == STREAK_LIMIT - 1) {
if (i == DEFAULT_STREAK_LIMIT - 1) {
ci.resetStreak();
}
}
@@ -64,7 +65,7 @@ public class ChannelImpressionsTest {
public void testStreakBlock() {
ChannelImpressions ci = new ChannelImpressions();
for (int i = 0; i <= STREAK_LIMIT; i++) {
for (int i = 0; i <= DEFAULT_STREAK_LIMIT; i++) {
ci.incrementViews();
ci.incrementDismissals();
}
@@ -76,7 +77,7 @@ public class ChannelImpressionsTest {
public void testRatio_NoBlockEvenWithStreak() {
ChannelImpressions ci = new ChannelImpressions();
for (int i = 0; i < STREAK_LIMIT; i++) {
for (int i = 0; i < DEFAULT_STREAK_LIMIT; i++) {
ci.incrementViews();
ci.incrementDismissals();
ci.incrementViews();
@@ -108,4 +109,53 @@ public class ChannelImpressionsTest {
// no crash
ci.append(null);
}
@Test
public void testUpdateThresholds_streakLimitsCorrectlyApplied() {
int updatedStreakLimit = DEFAULT_STREAK_LIMIT + 3;
ChannelImpressions ci = new ChannelImpressions();
ci.updateThresholds(DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT, updatedStreakLimit);
for (int i = 0; i <= updatedStreakLimit; i++) {
ci.incrementViews();
ci.incrementDismissals();
}
ChannelImpressions ci2 = new ChannelImpressions();
ci2.updateThresholds(DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT, updatedStreakLimit);
for (int i = 0; i < updatedStreakLimit; i++) {
ci2.incrementViews();
ci2.incrementDismissals();
}
assertTrue(ci.shouldTriggerBlock());
assertFalse(ci2.shouldTriggerBlock());
}
@Test
public void testUpdateThresholds_ratioLimitsCorrectlyApplied() {
float updatedDismissRatio = .99f;
ChannelImpressions ci = new ChannelImpressions();
ci.updateThresholds(updatedDismissRatio, DEFAULT_STREAK_LIMIT);
// N views, N-1 dismissals, which doesn't satisfy the ratio = 1 criteria.
for (int i = 0; i <= DEFAULT_STREAK_LIMIT; i++) {
ci.incrementViews();
if (i != DEFAULT_STREAK_LIMIT) {
ci.incrementDismissals();
}
}
ChannelImpressions ci2 = new ChannelImpressions();
ci2.updateThresholds(updatedDismissRatio, DEFAULT_STREAK_LIMIT);
for (int i = 0; i <= DEFAULT_STREAK_LIMIT; i++) {
ci2.incrementViews();
ci2.incrementDismissals();
}
assertFalse(ci.shouldTriggerBlock());
assertTrue(ci2.shouldTriggerBlock());
}
}