Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bf4f38d4 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Persist notification blocker info across reboots"

parents f904f325 3d5b3c7e
Loading
Loading
Loading
Loading
+169 −21
Original line number Diff line number Diff line
@@ -23,16 +23,37 @@ import static android.service.notification.NotificationListenerService.Ranking
import android.app.INotificationManager;
import android.content.Context;
import android.ext.services.R;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.service.notification.Adjustment;
import android.service.notification.NotificationAssistantService;
import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Slog;
import android.util.Xml;

import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;

import libcore.io.IoUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Map;

/**
 * Notification assistant that provides guidance on notification channel blocking
@@ -41,19 +62,112 @@ public class Assistant extends NotificationAssistantService {
    private static final String TAG = "ExtAssistant";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private static final ArrayList<Integer> DISMISS_WITH_PREJUDICE = new ArrayList<>();
    private static final String TAG_ASSISTANT = "assistant";
    private static final String TAG_IMPRESSION = "impression-set";
    private static final String ATT_KEY = "key";
    private static final int DB_VERSION = 1;
    private static final String ATTR_VERSION = "version";

    private static final ArrayList<Integer> PREJUDICAL_DISMISSALS = new ArrayList<>();
    static {
        DISMISS_WITH_PREJUDICE.add(REASON_CANCEL);
        DISMISS_WITH_PREJUDICE.add(REASON_LISTENER_CANCEL);
        PREJUDICAL_DISMISSALS.add(REASON_CANCEL);
        PREJUDICAL_DISMISSALS.add(REASON_LISTENER_CANCEL);
    }

    // key : impressions tracker
    // TODO: persist across reboots
    // TODO: prune deleted channels and apps
    ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>();
    // SBN key : channel id
    ArrayMap<String, String> mLiveNotifications = new ArrayMap<>();

    private Ranking mFakeRanking = null;
    private AtomicFile mFile = null;

    public Assistant() {
    }

    private void loadFile() {
        if (DEBUG) Slog.d(TAG, "loadFile");
        AsyncTask.execute(() -> {
            InputStream infile = null;
            try {
                infile = mFile.openRead();
                readXml(infile);
            } catch (FileNotFoundException e) {
                // No data yet
            } catch (IOException e) {
                Log.e(TAG, "Unable to read channel impressions", e);
            } catch (NumberFormatException | XmlPullParserException e) {
                Log.e(TAG, "Unable to parse channel impressions", e);
            } finally {
                IoUtils.closeQuietly(infile);
            }
        });
    }

    protected void readXml(InputStream stream)
            throws XmlPullParserException, NumberFormatException, IOException {
        final XmlPullParser parser = Xml.newPullParser();
        parser.setInput(stream, StandardCharsets.UTF_8.name());
        final int outerDepth = parser.getDepth();
        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
            if (!TAG_ASSISTANT.equals(parser.getName())) {
                continue;
            }
            final int impressionOuterDepth = parser.getDepth();
            while (XmlUtils.nextElementWithin(parser, impressionOuterDepth)) {
                if (!TAG_IMPRESSION.equals(parser.getName())) {
                    continue;
                }
                String key = parser.getAttributeValue(null, ATT_KEY);
                ChannelImpressions ci = new ChannelImpressions();
                ci.populateFromXml(parser);
                synchronized (mkeyToImpressions) {
                    ci.append(mkeyToImpressions.get(key));
                    mkeyToImpressions.put(key, ci);
                }
            }
        }
    }

    private void saveFile() throws IOException {
        AsyncTask.execute(() -> {
            final FileOutputStream stream;
            try {
                stream = mFile.startWrite();
            } catch (IOException e) {
                Slog.w(TAG, "Failed to save policy file", e);
                return;
            }
            try {
                final XmlSerializer out = new FastXmlSerializer();
                out.setOutput(stream, StandardCharsets.UTF_8.name());
                writeXml(out);
                mFile.finishWrite(stream);
            } catch (IOException e) {
                Slog.w(TAG, "Failed to save impressions file, restoring backup", e);
                mFile.failWrite(stream);
            }
        });
    }

    protected void writeXml(XmlSerializer out) throws IOException {
        out.startDocument(null, true);
        out.startTag(null, TAG_ASSISTANT);
        out.attribute(null, ATTR_VERSION, Integer.toString(DB_VERSION));
        synchronized (mkeyToImpressions) {
            for (Map.Entry<String, ChannelImpressions> entry
                    : mkeyToImpressions.entrySet()) {
                // TODO: ensure channel still exists
                out.startTag(null, TAG_IMPRESSION);
                out.attribute(null, ATT_KEY, entry.getKey());
                entry.getValue().writeXml(out);
                out.endTag(null, TAG_IMPRESSION);
            }
        }
        out.endTag(null, TAG_ASSISTANT);
        out.endDocument();
    }

    @Override
    public Adjustment onNotificationEnqueued(StatusBarNotification sbn) {
@@ -87,26 +201,38 @@ public class Assistant extends NotificationAssistantService {
    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
            NotificationStats stats, int reason) {
        try {
            boolean updatedImpressions = false;
            String channelId = mLiveNotifications.remove(sbn.getKey());
            String key = getKey(sbn.getPackageName(), sbn.getUserId(), channelId);
            ChannelImpressions ci = mkeyToImpressions.getOrDefault(key, new ChannelImpressions());
            synchronized (mkeyToImpressions) {
                ChannelImpressions ci = mkeyToImpressions.getOrDefault(key,
                        new ChannelImpressions());
                if (stats.hasSeen()) {
                    ci.incrementViews();
                    updatedImpressions = true;
                }
            if (DISMISS_WITH_PREJUDICE.contains(reason)
                    && !sbn.isAppGroup()
                    && !sbn.getNotification().isGroupChild()
                if (PREJUDICAL_DISMISSALS.contains(reason)) {
                    if ((!sbn.isAppGroup() || sbn.getNotification().isGroupChild())
                            && !stats.hasInteracted()
                            && stats.getDismissalSurface() != NotificationStats.DISMISSAL_AOD
                            && stats.getDismissalSurface() != NotificationStats.DISMISSAL_PEEK
                            && stats.getDismissalSurface() != NotificationStats.DISMISSAL_OTHER) {
               if (DEBUG) Log.i(TAG, "increment dismissals");
                        if (DEBUG) Log.i(TAG, "increment dismissals " + key);
                        ci.incrementDismissals();
                        updatedImpressions = true;
                    } else {
                if (DEBUG) Slog.i(TAG, "reset streak");
                        if (DEBUG) Slog.i(TAG, "reset streak " + key);
                        if (ci.getStreak() > 0) {
                            updatedImpressions = true;
                        }
                        ci.resetStreak();
                    }
                }
                mkeyToImpressions.put(key, ci);
            }
            if (updatedImpressions) {
                saveFile();
            }
        } catch (Throwable e) {
            Slog.e(TAG, "Error occurred processing removal", e);
        }
@@ -121,6 +247,11 @@ public class Assistant extends NotificationAssistantService {
    public void onListenerConnected() {
        if (DEBUG) Log.i(TAG, "CONNECTED");
        try {
            mFile = new AtomicFile(new File(new File(
                    Environment.getDataUserCePackageDirectory(
                            StorageManager.UUID_PRIVATE_INTERNAL, getUserId(), getPackageName()),
                    "assistant"), "block_stats.xml"));
            loadFile();
            for (StatusBarNotification sbn : getActiveNotifications()) {
                onNotificationPosted(sbn);
            }
@@ -129,7 +260,7 @@ public class Assistant extends NotificationAssistantService {
        }
    }

    private String getKey(String pkg, int userId, String channelId) {
    protected String getKey(String pkg, int userId, String channelId) {
        return pkg + "|" + userId + "|" + channelId;
    }

@@ -151,6 +282,11 @@ public class Assistant extends NotificationAssistantService {
    }

    // for testing

    protected void setFile(AtomicFile file) {
        mFile = file;
    }

    protected void setFakeRanking(Ranking ranking) {
        mFakeRanking = ranking;
    }
@@ -162,4 +298,16 @@ public class Assistant extends NotificationAssistantService {
    protected void setContext(Context context) {
        mSystemContext = context;
    }

    protected ChannelImpressions getImpressions(String key) {
        synchronized (mkeyToImpressions) {
            return mkeyToImpressions.get(key);
        }
    }

    protected void insertImpressions(String key, ChannelImpressions ci) {
        synchronized (mkeyToImpressions) {
            mkeyToImpressions.put(key, ci);
        }
    }
}
 No newline at end of file
+49 −0
Original line number Diff line number Diff line
@@ -18,14 +18,23 @@ package android.ext.services.notification;

import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;

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 = .8;
    static final int STREAK_LIMIT = 2;
    static final String ATT_DISMISSALS = "dismisses";
    static final String ATT_VIEWS = "views";
    static final String ATT_STREAK = "streak";

    private int mDismissals = 0;
    private int mViews = 0;
@@ -62,6 +71,14 @@ public final class ChannelImpressions implements Parcelable {
        mStreak++;
    }

    public void append(ChannelImpressions additionalImpressions) {
        if (additionalImpressions != null) {
            mViews += additionalImpressions.getViews();
            mStreak += additionalImpressions.getStreak();
            mDismissals += additionalImpressions.getDismissals();
        }
    }

    public void incrementViews() {
        mViews++;
    }
@@ -134,4 +151,36 @@ public final class ChannelImpressions implements Parcelable {
        sb.append('}');
        return sb.toString();
    }

    protected void populateFromXml(XmlPullParser parser) {
        mDismissals = safeInt(parser, ATT_DISMISSALS, 0);
        mStreak = safeInt(parser, ATT_STREAK, 0);
        mViews = safeInt(parser, ATT_VIEWS, 0);
    }

    protected void writeXml(XmlSerializer out) throws IOException {
        if (mDismissals != 0) {
            out.attribute(null, ATT_DISMISSALS, String.valueOf(mDismissals));
        }
        if (mStreak != 0) {
            out.attribute(null, ATT_STREAK, String.valueOf(mStreak));
        }
        if (mViews != 0) {
            out.attribute(null, ATT_VIEWS, String.valueOf(mViews));
        }
    }

    private static int safeInt(XmlPullParser parser, String att, int defValue) {
        final String val = parser.getAttributeValue(null, att);
        return tryParseInt(val, defValue);
    }

    private static int tryParseInt(String value, int defValue) {
        if (TextUtils.isEmpty(value)) return defValue;
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return defValue;
        }
    }
}
+134 −8
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.content.Intent;
import android.ext.services.R;
import android.os.UserHandle;
import android.service.notification.Adjustment;
import android.service.notification.NotificationListenerService;
@@ -42,6 +41,10 @@ import android.service.notification.StatusBarNotification;
import android.support.test.InstrumentationRegistry;
import android.test.ServiceTestCase;
import android.testing.TestableContext;
import android.util.AtomicFile;
import android.util.Xml;

import com.android.internal.util.FastXmlSerializer;

import org.junit.Before;
import org.junit.Rule;
@@ -49,6 +52,14 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;

public class AssistantTest extends ServiceTestCase<Assistant> {

@@ -67,6 +78,8 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
            new NotificationChannel("one", "", IMPORTANCE_LOW);

    @Mock INotificationManager mNoMan;
    @Mock
    AtomicFile mFile;

    Assistant mAssistant;

@@ -88,6 +101,8 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
        bindService(startIntent);
        mAssistant = getService();
        mAssistant.setNoMan(mNoMan);
        mAssistant.setFile(mFile);
        when(mFile.startWrite()).thenReturn(mock(FileOutputStream.class));
    }

    private StatusBarNotification generateSbn(String pkg, int uid, NotificationChannel channel,
@@ -170,18 +185,43 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
    }

    @Test
    public void testGroupCannotTriggerAdjustment() throws Exception {
    public void testGroupChildCanTriggerAdjustment() throws Exception {
        almostBlockChannel(PKG1, UID1, P1C1);

        StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", "I HAVE A GROUP");
        mAssistant.setFakeRanking(mock(Ranking.class));
        mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
        NotificationStats stats = new NotificationStats();
        stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
        stats.setSeen();
        mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
        mAssistant.onNotificationRemoved(
                sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);

        sbn = generateSbn(PKG1, UID1, P1C1, "new one!", null);
        sbn = generateSbn(PKG1, UID1, P1C1, "new one!", "group");
        mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));

        ArgumentCaptor<Adjustment> captor = ArgumentCaptor.forClass(Adjustment.class);
        verify(mNoMan, times(1)).applyAdjustmentFromAssistant(any(), captor.capture());
        assertEquals(sbn.getKey(), captor.getValue().getKey());
        assertEquals(Ranking.USER_SENTIMENT_NEGATIVE,
                captor.getValue().getSignals().getInt(Adjustment.KEY_USER_SENTIMENT));
    }

    @Test
    public void testGroupSummaryCannotTriggerAdjustment() throws Exception {
        almostBlockChannel(PKG1, UID1, P1C1);

        StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", "I HAVE A GROUP");
        sbn.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
        mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
        NotificationStats stats = new NotificationStats();
        stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
        stats.setSeen();
        mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
        mAssistant.onNotificationRemoved(
                sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);

        sbn = generateSbn(PKG1, UID1, P1C1, "new one!", "group");
        mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));

        verify(mNoMan, never()).applyAdjustmentFromAssistant(any(), any());
@@ -192,10 +232,11 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
        almostBlockChannel(PKG1, UID1, P1C1);

        StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", null);
        mAssistant.setFakeRanking(mock(Ranking.class));
        mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
        NotificationStats stats = new NotificationStats();
        stats.setDismissalSurface(NotificationStats.DISMISSAL_AOD);
        stats.setSeen();
        mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
        mAssistant.onNotificationRemoved(
                sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);

@@ -208,13 +249,13 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
    @Test
    public void testInteractedCannotTriggerAdjustment() throws Exception {
        almostBlockChannel(PKG1, UID1, P1C1);

        StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", null);
        mAssistant.setFakeRanking(mock(Ranking.class));
        mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
        NotificationStats stats = new NotificationStats();
        stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
        stats.setSeen();
        stats.setExpanded();
        mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
        mAssistant.onNotificationRemoved(
                sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);

@@ -229,10 +270,11 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
        almostBlockChannel(PKG1, UID1, P1C1);

        StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", null);
        mAssistant.setFakeRanking(mock(Ranking.class));
        mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
        NotificationStats stats = new NotificationStats();
        stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
        stats.setSeen();
        mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
        mAssistant.onNotificationRemoved(
                sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_APP_CANCEL);

@@ -265,4 +307,88 @@ public class AssistantTest extends ServiceTestCase<Assistant> {

        verify(mNoMan, never()).applyAdjustmentFromAssistant(any(), any());
    }

    @Test
    public void testReadXml() throws Exception {
        String key1 = mAssistant.getKey("pkg1", 1, "channel1");
        int streak1 = 2;
        int views1 = 5;
        int dismiss1 = 9;

        int streak1a = 3;
        int views1a = 10;
        int dismiss1a = 99;
        String key1a = mAssistant.getKey("pkg1", 1, "channel1a");

        int streak2 = 7;
        int views2 = 77;
        int dismiss2 = 777;
        String key2 = mAssistant.getKey("pkg2", 2, "channel2");

        String xml = "<assistant version=\"1\">\n"
                + "<impression-set key=\"" + key1 + "\" "
                + "dismisses=\"" + dismiss1 + "\" views=\"" + views1
                + "\" streak=\"" + streak1 + "\"/>\n"
                + "<impression-set key=\"" + key1a + "\" "
                + "dismisses=\"" + dismiss1a + "\" views=\"" + views1a
                + "\" streak=\"" + streak1a + "\"/>\n"
                + "<impression-set key=\"" + key2 + "\" "
                + "dismisses=\"" + dismiss2 + "\" views=\"" + views2
                + "\" streak=\"" + streak2 + "\"/>\n"
                + "</assistant>\n";
        mAssistant.readXml(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())));

        ChannelImpressions c1 = mAssistant.getImpressions(key1);
        assertEquals(2, c1.getStreak());
        assertEquals(5, c1.getViews());
        assertEquals(9, c1.getDismissals());

        ChannelImpressions c1a = mAssistant.getImpressions(key1a);
        assertEquals(3, c1a.getStreak());
        assertEquals(10, c1a.getViews());
        assertEquals(99, c1a.getDismissals());

        ChannelImpressions c2 = mAssistant.getImpressions(key2);
        assertEquals(7, c2.getStreak());
        assertEquals(77, c2.getViews());
        assertEquals(777, c2.getDismissals());
    }

    @Test
    public void testRoundTripXml() throws Exception {
        String key1 = mAssistant.getKey("pkg1", 1, "channel1");
        ChannelImpressions ci1 = new ChannelImpressions(9, 10);
        String key2 = mAssistant.getKey("pkg1", 1, "channel2");
        ChannelImpressions ci2 = new ChannelImpressions();
        for (int i = 0; i < 3; i++) {
            ci2.incrementViews();
            ci2.incrementDismissals();
        }
        ChannelImpressions ci3 = new ChannelImpressions();
        String key3 = mAssistant.getKey("pkg3", 3, "channel2");
        for (int i = 0; i < 9; i++) {
            ci3.incrementViews();
            if (i % 3 == 0) {
                ci3.incrementDismissals();
            }
        }

        mAssistant.insertImpressions(key1, ci1);
        mAssistant.insertImpressions(key2, ci2);
        mAssistant.insertImpressions(key3, ci3);


        XmlSerializer serializer = new FastXmlSerializer();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
        mAssistant.writeXml(serializer);

        Assistant assistant = new Assistant();
        assistant.readXml(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())));

        assertEquals(ci1, assistant.getImpressions(key1));
        assertEquals(ci2, assistant.getImpressions(key2));
        assertEquals(ci3, assistant.getImpressions(key3));
    }

}
+26 −0
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import static android.ext.services.notification.ChannelImpressions.STREAK_LIMIT;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class ChannelImpressionsTest {
@@ -82,4 +84,28 @@ public class ChannelImpressionsTest {

        assertFalse(ci.shouldTriggerBlock());
    }

    @Test
    public void testAppend() {
        ChannelImpressions ci = new ChannelImpressions();
        ci.incrementViews();
        ci.incrementDismissals();

        ChannelImpressions ci2 = new ChannelImpressions();
        ci2.incrementViews();
        ci2.incrementDismissals();
        ci2.incrementViews();

        ci.append(ci2);
        assertEquals(3, ci.getViews());
        assertEquals(2, ci.getDismissals());
        assertEquals(2, ci.getStreak());

        assertEquals(2, ci2.getViews());
        assertEquals(1, ci2.getDismissals());
        assertEquals(1, ci2.getStreak());

        // no crash
        ci.append(null);
    }
}
+14 −14
Original line number Diff line number Diff line
@@ -25,6 +25,20 @@ import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.fail;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
@@ -74,20 +88,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class RankingHelperTest extends NotificationTestCase {