diff options
5 files changed, 392 insertions, 43 deletions
diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java index f535368bd356..6fe8975577a4 100644 --- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java +++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java @@ -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()); - if (stats.hasSeen()) { - ci.incrementViews(); + synchronized (mkeyToImpressions) { + ChannelImpressions ci = mkeyToImpressions.getOrDefault(key, + new ChannelImpressions()); + if (stats.hasSeen()) { + ci.incrementViews(); + updatedImpressions = true; + } + 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 " + key); + ci.incrementDismissals(); + updatedImpressions = true; + } else { + if (DEBUG) Slog.i(TAG, "reset streak " + key); + if (ci.getStreak() > 0) { + updatedImpressions = true; + } + ci.resetStreak(); + } + } + mkeyToImpressions.put(key, ci); } - if (DISMISS_WITH_PREJUDICE.contains(reason) - && !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"); - ci.incrementDismissals(); - } else { - if (DEBUG) Slog.i(TAG, "reset streak"); - ci.resetStreak(); + if (updatedImpressions) { + saveFile(); } - mkeyToImpressions.put(key, ci); } 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 diff --git a/packages/ExtServices/src/android/ext/services/notification/ChannelImpressions.java b/packages/ExtServices/src/android/ext/services/notification/ChannelImpressions.java index 30567ccdd074..4ad4b2415735 100644 --- a/packages/ExtServices/src/android/ext/services/notification/ChannelImpressions.java +++ b/packages/ExtServices/src/android/ext/services/notification/ChannelImpressions.java @@ -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; + } + } } diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java index 4e5e9f9d1e00..7c35b48310f2 100644 --- a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java +++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java @@ -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)); + } + } diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/ChannelImpressionsTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/ChannelImpressionsTest.java index a8c9fa35e678..d28e2ac0cffe 100644 --- a/packages/ExtServices/tests/src/android/ext/services/notification/ChannelImpressionsTest.java +++ b/packages/ExtServices/tests/src/android/ext/services/notification/ChannelImpressionsTest.java @@ -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); + } } diff --git a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java index df989f7d6be5..3c02e23eed2a 100644 --- a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java @@ -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 { |