Sysui bubbles multiuser fixes: persistence / overflow

* Persist bubbles per-user - rather than one list the
  XML now has a list per-user. The entries in these lists
  still include userId for workprofile since bubbles are
  mixed in the stack / overflow for workprofile.
* When loading bubbles, only the ones for the current user
  are loaded / hit bubbleController code
* When user changes, overflow data should be re-loaded
* Allow the bubble window to be visible for all users

Test: atest BubbleXmlHelperTest BubbleVolatileRepositoryTest
BubblePersistentRepositoryTest BubblesTest
Bug: 173408780

Change-Id: I88cb7cc7ee676d8e0756328a95a54fdaf018a013
This commit is contained in:
Mady Mellor
2021-02-01 11:23:30 -08:00
parent b5a783d611
commit bac16b93bc
15 changed files with 724 additions and 389 deletions

View File

@@ -48,6 +48,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.PointF;
@@ -64,6 +65,7 @@ import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseSetArray;
import android.view.View;
import android.view.ViewGroup;
@@ -144,6 +146,8 @@ public class BubbleController {
// Tracks the id of the current (foreground) user.
private int mCurrentUserId;
// Current profiles of the user (e.g. user with a workprofile)
private SparseArray<UserInfo> mCurrentProfiles;
// Saves notification keys of active bubbles when users are switched.
private final SparseSetArray<String> mSavedBubbleKeysPerUser;
@@ -153,8 +157,8 @@ public class BubbleController {
// Callback that updates BubbleOverflowActivity on data change.
@Nullable private BubbleData.Listener mOverflowListener = null;
// Only load overflow data from disk once
private boolean mOverflowDataLoaded = false;
// Typically only load once & after user switches
private boolean mOverflowDataLoadNeeded = true;
/**
* When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select
@@ -468,14 +472,31 @@ public class BubbleController {
updateStack();
}
private void onUserChanged(int newUserId) {
/** Called when the current user changes. */
@VisibleForTesting
public void onUserChanged(int newUserId) {
saveBubbles(mCurrentUserId);
mBubbleData.dismissAll(DISMISS_USER_CHANGED);
restoreBubbles(newUserId);
mCurrentUserId = newUserId;
mBubbleData.dismissAll(DISMISS_USER_CHANGED);
mBubbleData.clearOverflow();
mOverflowDataLoadNeeded = true;
restoreBubbles(newUserId);
mBubbleData.setCurrentUserId(newUserId);
}
/** Called when the profiles for the current user change. **/
public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
mCurrentProfiles = currentProfiles;
}
/** Whether this userId belongs to the current user. */
private boolean isCurrentProfile(int userId) {
return userId == UserHandle.USER_ALL
|| (mCurrentProfiles != null && mCurrentProfiles.get(userId) != null);
}
/**
* Sets whether to perform inflation on the same thread as the caller. This method should only
* be used in tests, not in production.
@@ -556,6 +577,7 @@ public class BubbleController {
mWmLayoutParams.setTitle("Bubbles!");
mWmLayoutParams.packageName = mContext.getPackageName();
mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
mWmLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
try {
mAddedToWindowManager = true;
@@ -639,7 +661,7 @@ public class BubbleController {
});
});
// Finally, remove the entries for this user now that bubbles are restored.
mSavedBubbleKeysPerUser.remove(mCurrentUserId);
mSavedBubbleKeysPerUser.remove(userId);
}
private void updateForThemeChanges() {
@@ -804,12 +826,12 @@ public class BubbleController {
* Fills the overflow bubbles by loading them from disk.
*/
void loadOverflowBubblesFromDisk() {
if (!mBubbleData.getOverflowBubbles().isEmpty() || mOverflowDataLoaded) {
if (!mBubbleData.getOverflowBubbles().isEmpty() && !mOverflowDataLoadNeeded) {
// we don't need to load overflow bubbles from disk if it is already in memory
return;
}
mOverflowDataLoaded = true;
mDataRepository.loadBubbles((bubbles) -> {
mOverflowDataLoadNeeded = false;
mDataRepository.loadBubbles(mCurrentUserId, (bubbles) -> {
bubbles.forEach(bubble -> {
if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) {
// if the bubble is already active, there's no need to push it to overflow
@@ -911,6 +933,12 @@ public class BubbleController {
Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key);
BubbleEntry entry = entryData.first;
boolean shouldBubbleUp = entryData.second;
if (entry != null && !isCurrentProfile(
entry.getStatusBarNotification().getUser().getIdentifier())) {
return;
}
rankingMap.getRanking(key, mTmpRanking);
boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key);
if (isActiveBubble && !mTmpRanking.canBubble()) {
@@ -1427,6 +1455,13 @@ public class BubbleController {
});
}
@Override
public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
mMainExecutor.execute(() -> {
BubbleController.this.onCurrentProfilesChanged(currentProfiles);
});
}
@Override
public void onConfigChanged(Configuration newConfig) {
mMainExecutor.execute(() -> {

View File

@@ -510,7 +510,8 @@ public class BubbleData {
|| reason == Bubbles.DISMISS_NO_LONGER_BUBBLE
|| reason == Bubbles.DISMISS_BLOCKED
|| reason == Bubbles.DISMISS_SHORTCUT_REMOVED
|| reason == Bubbles.DISMISS_PACKAGE_REMOVED)) {
|| reason == Bubbles.DISMISS_PACKAGE_REMOVED
|| reason == Bubbles.DISMISS_USER_CHANGED)) {
Bubble b = getOverflowBubbleWithKey(key);
if (DEBUG_BUBBLE_DATA) {
@@ -642,6 +643,16 @@ public class BubbleData {
}
}
/**
* Removes all bubbles from the overflow, called when the user changes.
*/
public void clearOverflow() {
while (!mOverflowBubbles.isEmpty()) {
doRemove(mOverflowBubbles.get(0).getKey(), Bubbles.DISMISS_USER_CHANGED);
}
dispatchPendingChanges();
}
private void dispatchPendingChanges() {
if (mListener != null && mStateChange.anythingChanged()) {
mListener.applyUpdate(mStateChange);

View File

@@ -58,7 +58,8 @@ internal class BubbleDataRepository(
*/
fun addBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
if (DEBUG) Log.d(TAG, "adding ${bubbles.size} bubbles")
val entities = transform(userId, bubbles).also(volatileRepository::addBubbles)
val entities = transform(bubbles).also {
b -> volatileRepository.addBubbles(userId, b) }
if (entities.isNotEmpty()) persistToDisk()
}
@@ -67,14 +68,15 @@ internal class BubbleDataRepository(
*/
fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
if (DEBUG) Log.d(TAG, "removing ${bubbles.size} bubbles")
val entities = transform(userId, bubbles).also(volatileRepository::removeBubbles)
val entities = transform(bubbles).also {
b -> volatileRepository.removeBubbles(userId, b) }
if (entities.isNotEmpty()) persistToDisk()
}
private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleEntity> {
private fun transform(bubbles: List<Bubble>): List<BubbleEntity> {
return bubbles.mapNotNull { b ->
BubbleEntity(
userId,
b.user.identifier,
b.packageName,
b.metadataShortcutId ?: return@mapNotNull null,
b.key,
@@ -116,10 +118,11 @@ internal class BubbleDataRepository(
/**
* Load bubbles from disk.
* @param cb The callback to be run after the bubbles are loaded. This callback is always made
* on the main thread of the hosting process.
* on the main thread of the hosting process. The callback is only run if there are
* bubbles.
*/
@SuppressLint("WrongConstant")
fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch {
fun loadBubbles(userId: Int, cb: (List<Bubble>) -> Unit) = ioScope.launch {
/**
* Load BubbleEntity from disk.
* e.g.
@@ -129,8 +132,9 @@ internal class BubbleDataRepository(
* BubbleEntity(0, "com.example.messenger", "id-1")
* ]
*/
val entities = persistentRepository.readFromDisk()
volatileRepository.addBubbles(entities)
val entitiesByUser = persistentRepository.readFromDisk()
val entities = entitiesByUser.get(userId) ?: return@launch
volatileRepository.addBubbles(userId, entities)
/**
* Extract userId/packageName from these entities.
* e.g.
@@ -139,9 +143,10 @@ internal class BubbleDataRepository(
* ]
*/
val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet()
/**
* Retrieve shortcuts with given userId/packageName combination, then construct a mapping
* from the userId/packageName pair to a list of associated ShortcutInfo.
* Retrieve shortcuts with given userId/packageName combination, then construct a
* mapping from the userId/packageName pair to a list of associated ShortcutInfo.
* e.g.
* {
* ShortcutKey(0, "com.example.messenger") -> [
@@ -161,21 +166,23 @@ internal class BubbleDataRepository(
.setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId))
?: emptyList()
}.groupBy { ShortcutKey(it.userId, it.`package`) }
// For each entity loaded from xml, find the corresponding ShortcutInfo then convert them
// into Bubble.
// For each entity loaded from xml, find the corresponding ShortcutInfo then convert
// them into Bubble.
val bubbles = entities.mapNotNull { entity ->
shortcutMap[ShortcutKey(entity.userId, entity.packageName)]
?.firstOrNull { shortcutInfo -> entity.shortcutId == shortcutInfo.id }
?.let { shortcutInfo -> Bubble(
entity.key,
shortcutInfo,
entity.desiredHeight,
entity.desiredHeightResId,
entity.title,
entity.taskId,
entity.locus,
mainExecutor
) }
?.let { shortcutInfo ->
Bubble(
entity.key,
shortcutInfo,
entity.desiredHeight,
entity.desiredHeightResId,
entity.title,
entity.taskId,
entity.locus,
mainExecutor
)
}
}
mainExecutor.execute { cb(bubbles) }
}

View File

@@ -217,10 +217,16 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) {
Objects.requireNonNull(context);
if (icon == null) return null;
if (icon.getType() == Icon.TYPE_URI || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
context.grantUriPermission(context.getPackageName(),
icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
if (icon.getType() == Icon.TYPE_URI
|| icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
context.grantUriPermission(context.getPackageName(),
icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
return icon.loadDrawable(context);
} catch (Exception e) {
Log.w(TAG, "loadSenderAvatar failed: " + e.getMessage());
return null;
}
return icon.loadDrawable(context);
}
}

View File

@@ -21,12 +21,14 @@ import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Looper;
import android.service.notification.NotificationListenerService.RankingMap;
import android.util.ArraySet;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
import androidx.annotation.IntDef;
@@ -213,6 +215,13 @@ public interface Bubbles {
*/
void onUserChanged(int newUserId);
/**
* Called when the current user profiles change.
*
* @param currentProfiles the user infos for the current profile.
*/
void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles);
/**
* Called when config changed.
*

View File

@@ -18,6 +18,7 @@ package com.android.wm.shell.bubbles.storage
import android.content.Context
import android.util.AtomicFile
import android.util.Log
import android.util.SparseArray
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -27,8 +28,8 @@ class BubblePersistentRepository(context: Context) {
private val bubbleFile: AtomicFile = AtomicFile(File(context.filesDir,
"overflow_bubbles.xml"), "overflow-bubbles")
fun persistsToDisk(bubbles: List<BubbleEntity>): Boolean {
if (DEBUG) Log.d(TAG, "persisting ${bubbles.size} bubbles")
fun persistsToDisk(bubbles: SparseArray<List<BubbleEntity>>): Boolean {
if (DEBUG) Log.d(TAG, "persisting ${bubbles.size()} bubbles")
synchronized(bubbleFile) {
val stream: FileOutputStream = try { bubbleFile.startWrite() } catch (e: IOException) {
Log.e(TAG, "Failed to save bubble file", e)
@@ -37,7 +38,7 @@ class BubblePersistentRepository(context: Context) {
try {
writeXml(stream, bubbles)
bubbleFile.finishWrite(stream)
if (DEBUG) Log.d(TAG, "persisted ${bubbles.size} bubbles")
if (DEBUG) Log.d(TAG, "persisted ${bubbles.size()} bubbles")
return true
} catch (e: Exception) {
Log.e(TAG, "Failed to save bubble file, restoring backup", e)
@@ -47,13 +48,13 @@ class BubblePersistentRepository(context: Context) {
return false
}
fun readFromDisk(): List<BubbleEntity> {
fun readFromDisk(): SparseArray<List<BubbleEntity>> {
synchronized(bubbleFile) {
if (!bubbleFile.exists()) return emptyList()
if (!bubbleFile.exists()) return SparseArray()
try { return bubbleFile.openRead().use(::readXml) } catch (e: Throwable) {
Log.e(TAG, "Failed to open bubble file", e)
}
return emptyList()
return SparseArray()
}
}
}

View File

@@ -17,6 +17,7 @@ package com.android.wm.shell.bubbles.storage
import android.content.pm.LauncherApps
import android.os.UserHandle
import android.util.SparseArray
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.bubbles.ShortcutKey
@@ -27,10 +28,11 @@ private const val CAPACITY = 16
* manipulation.
*/
class BubbleVolatileRepository(private val launcherApps: LauncherApps) {
/**
* An ordered set of bubbles based on their natural ordering.
* Set of bubbles per user. Each set of bubbles is ordered by recency.
*/
private var entities = mutableSetOf<BubbleEntity>()
private var entitiesByUser = SparseArray<MutableList<BubbleEntity>>()
/**
* The capacity of the cache.
@@ -39,19 +41,43 @@ class BubbleVolatileRepository(private val launcherApps: LauncherApps) {
var capacity = CAPACITY
/**
* Returns a snapshot of all the bubbles.
* Returns a snapshot of all the bubbles, a map of the userId to bubble list.
*/
val bubbles: List<BubbleEntity>
val bubbles: SparseArray<List<BubbleEntity>>
@Synchronized
get() = entities.toList()
get() {
val map = SparseArray<List<BubbleEntity>>()
for (i in 0 until entitiesByUser.size()) {
val k = entitiesByUser.keyAt(i)
val v = entitiesByUser.valueAt(i)
map.put(k, v.toList())
}
return map
}
/**
* Returns the entity list of the provided user's bubbles or creates one if it doesn't exist.
*/
@Synchronized
fun getEntities(userId: Int): MutableList<BubbleEntity> {
val entities = entitiesByUser.get(userId)
return when (entities) {
null -> mutableListOf<BubbleEntity>().also {
entitiesByUser.put(userId, it)
}
else -> entities
}
}
/**
* Add the bubbles to memory and perform a de-duplication. In case a bubble already exists,
* it will be moved to the last.
*/
@Synchronized
fun addBubbles(bubbles: List<BubbleEntity>) {
fun addBubbles(userId: Int, bubbles: List<BubbleEntity>) {
if (bubbles.isEmpty()) return
// Get the list for this user
var entities = getEntities(userId)
// Verify the size of given bubbles is within capacity, otherwise trim down to capacity
val bubblesInRange = bubbles.takeLast(capacity)
// To ensure natural ordering of the bubbles, removes bubbles which already exist
@@ -61,16 +87,17 @@ class BubbleVolatileRepository(private val launcherApps: LauncherApps) {
if (overflowCount > 0) {
// Uncache ShortcutInfo of bubbles that will be removed due to capacity
uncache(entities.take(overflowCount))
entities = entities.drop(overflowCount).toMutableSet()
entities = entities.drop(overflowCount).toMutableList()
}
entities.addAll(bubblesInRange)
entitiesByUser.put(userId, entities)
cache(uniqueBubbles)
}
@Synchronized
fun removeBubbles(bubbles: List<BubbleEntity>) =
fun removeBubbles(userId: Int, bubbles: List<BubbleEntity>) =
uncache(bubbles.filter { b: BubbleEntity ->
entities.removeIf { e: BubbleEntity -> b.key == e.key } })
getEntities(userId).removeIf { e: BubbleEntity -> b.key == e.key } })
private fun cache(bubbles: List<BubbleEntity>) {
bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) ->

View File

@@ -16,6 +16,8 @@
package com.android.wm.shell.bubbles.storage
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.os.UserHandle
import android.util.SparseArray
import android.util.Xml
import com.android.internal.util.FastXmlSerializer
import com.android.internal.util.XmlUtils
@@ -26,8 +28,8 @@ import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.StandardCharsets
// TODO: handle version changes gracefully
private const val CURRENT_VERSION = 1
// If this number increases, consider bubbles might be restored even with differences in XML.
private const val CURRENT_VERSION = 2
private const val TAG_BUBBLES = "bs"
private const val ATTR_VERSION = "v"
@@ -46,13 +48,20 @@ private const val ATTR_LOCUS = "l"
* Writes the bubbles in xml format into given output stream.
*/
@Throws(IOException::class)
fun writeXml(stream: OutputStream, bubbles: List<BubbleEntity>) {
fun writeXml(stream: OutputStream, bubbles: SparseArray<List<BubbleEntity>>) {
val serializer: XmlSerializer = FastXmlSerializer()
serializer.setOutput(stream, StandardCharsets.UTF_8.name())
serializer.startDocument(null, true)
serializer.startTag(null, TAG_BUBBLES)
serializer.attribute(null, ATTR_VERSION, CURRENT_VERSION.toString())
bubbles.forEach { b -> writeXmlEntry(serializer, b) }
for (i in 0 until bubbles.size()) {
val k = bubbles.keyAt(i)
val v = bubbles.valueAt(i)
serializer.startTag(null, TAG_BUBBLES)
serializer.attribute(null, ATTR_USER_ID, k.toString())
v.forEach { b -> writeXmlEntry(serializer, b) }
serializer.endTag(null, TAG_BUBBLES)
}
serializer.endTag(null, TAG_BUBBLES)
serializer.endDocument()
}
@@ -84,16 +93,39 @@ private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleEntity) {
/**
* Reads the bubbles from xml file.
*/
fun readXml(stream: InputStream): List<BubbleEntity> {
val bubbles = mutableListOf<BubbleEntity>()
fun readXml(stream: InputStream): SparseArray<List<BubbleEntity>> {
val bubbles = SparseArray<List<BubbleEntity>>()
val parser: XmlPullParser = Xml.newPullParser()
parser.setInput(stream, StandardCharsets.UTF_8.name())
XmlUtils.beginDocument(parser, TAG_BUBBLES)
val version = parser.getAttributeWithName(ATTR_VERSION)?.toInt()
if (version != null && version == CURRENT_VERSION) {
val veryOuterDepth = parser.depth
val version = parser.getAttributeWithName(ATTR_VERSION)?.toInt() ?: return bubbles
if (version == CURRENT_VERSION) {
while (XmlUtils.nextElementWithin(parser, veryOuterDepth)) {
val uid = parser.getAttributeWithName(ATTR_USER_ID) ?: continue
val outerDepth = parser.depth
val userBubbles = mutableListOf<BubbleEntity>()
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
userBubbles.add(readXmlEntry(parser) ?: continue)
}
if (!userBubbles.isEmpty()) {
bubbles.put(uid.toInt(), userBubbles.toList())
}
}
} else if (version == 1) {
// upgrade v1 to v2 format
val outerDepth = parser.depth
val userBubbles = mutableListOf<BubbleEntity>()
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
bubbles.add(readXmlEntry(parser) ?: continue)
// We can't tell which profile the bubble was for, so we'll only copy the main users'
// bubbles on upgrade.
val b = readXmlEntry(parser)
if (b != null && b.userId == UserHandle.USER_SYSTEM) {
userBubbles.add(b)
}
}
if (!userBubbles.isEmpty()) {
bubbles.put(UserHandle.USER_SYSTEM, userBubbles.toList())
}
}
return bubbles

View File

@@ -18,8 +18,10 @@ package com.android.wm.shell.bubbles.storage
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.testing.AndroidTestingRunner
import android.util.SparseArray
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.bubbles.storage.BubbleXmlHelperTest.Companion.sparseArraysEqual
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertNotNull
import junit.framework.Assert.assertTrue
@@ -31,19 +33,32 @@ import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class)
class BubblePersistentRepositoryTest : ShellTestCase() {
private val bubbles = listOf(
// user, package, shortcut, notification key, height, res-height, title, taskId, locusId
BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1", 120, 0, null, 1, null),
BubbleEntity(10, "com.example.chat", "alice and bob", "key-2", 0, 16537428, "title",
2, null),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3", 120, 0, null,
INVALID_TASK_ID, "key-3")
// user, package, shortcut, notification key, height, res-height, title, taskId, locusId
private val user0Bubbles = listOf(
BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1, null),
BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2,
null),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null,
INVALID_TASK_ID, null)
)
private val user1Bubbles = listOf(
BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3, null),
BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4,
null),
BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null,
INVALID_TASK_ID, null)
)
private val bubbles = SparseArray<List<BubbleEntity>>()
private lateinit var repository: BubblePersistentRepository
@Before
fun setup() {
repository = BubblePersistentRepository(mContext)
bubbles.put(0, user0Bubbles)
bubbles.put(1, user1Bubbles)
}
@Test
@@ -51,9 +66,9 @@ class BubblePersistentRepositoryTest : ShellTestCase() {
// Verify read before write doesn't cause FileNotFoundException
val actual = repository.readFromDisk()
assertNotNull(actual)
assertTrue(actual.isEmpty())
assertEquals(actual.size(), 0)
repository.persistsToDisk(bubbles)
assertEquals(bubbles, repository.readFromDisk())
assertTrue(sparseArraysEqual(bubbles, repository.readFromDisk()))
}
}

View File

@@ -21,32 +21,40 @@ import android.content.pm.LauncherApps
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import org.junit.Test
import com.android.wm.shell.ShellTestCase
import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.reset
@SmallTest
@RunWith(AndroidTestingRunner::class)
class BubbleVolatileRepositoryTest : ShellTestCase() {
private val user0 = UserHandle.of(0)
private val user10 = UserHandle.of(10)
private val user10_managed = UserHandle.of(10) // In test, acts as workprofile of user0
private val user11 = UserHandle.of(11)
// user, package, shortcut, notification key, height, res-height, title, taskId, locusId
private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1", 120, 0,
null, 1, null)
private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1",
"0key-1", 120, 0, null, 1, null)
private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob",
"key-2", 0, 16537428, "title", 2, null)
private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3", 120, 0,
null, INVALID_TASK_ID, "key-3")
"10key-2", 0, 16537428, "title", 2, null)
private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2",
"0key-3", 120, 0, null, INVALID_TASK_ID, null)
private val bubbles = listOf(bubble1, bubble2, bubble3)
private val bubble11 = BubbleEntity(11, "com.example.messenger",
"shortcut-1", "01key-1", 120, 0, null, 3)
private val bubble12 = BubbleEntity(11, "com.example.chat", "alice and bob",
"11key-2", 0, 16537428, "title", INVALID_TASK_ID)
private val user0bubbles = listOf(bubble1, bubble2, bubble3)
private val user11bubbles = listOf(bubble11, bubble12)
private lateinit var repository: BubbleVolatileRepository
private lateinit var launcherApps: LauncherApps
@@ -59,51 +67,74 @@ class BubbleVolatileRepositoryTest : ShellTestCase() {
@Test
fun testAddBubbles() {
repository.addBubbles(bubbles)
assertEquals(bubbles, repository.bubbles)
repository.addBubbles(user0.identifier, user0bubbles)
repository.addBubbles(user11.identifier, user11bubbles)
assertEquals(user0bubbles, repository.getEntities(user0.identifier).toList())
assertEquals(user11bubbles, repository.getEntities(user11.identifier).toList())
verify(launcherApps).cacheShortcuts(eq(PKG_MESSENGER),
eq(listOf("shortcut-1", "shortcut-2")), eq(user0),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
verify(launcherApps).cacheShortcuts(eq(PKG_CHAT),
eq(listOf("alice and bob")), eq(user10),
eq(listOf("alice and bob")), eq(user10_managed),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
repository.addBubbles(listOf(bubble1))
assertEquals(listOf(bubble2, bubble3, bubble1), repository.bubbles)
verifyNoMoreInteractions(launcherApps)
verify(launcherApps).cacheShortcuts(eq(PKG_MESSENGER),
eq(listOf("shortcut-1")), eq(user11),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
verify(launcherApps).cacheShortcuts(eq(PKG_CHAT),
eq(listOf("alice and bob")), eq(user11),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
repository.addBubbles(user0.identifier, listOf(bubble1))
assertEquals(listOf(bubble2, bubble3, bubble1), repository.getEntities(user0.identifier))
repository.addBubbles(user11.identifier, listOf(bubble12))
assertEquals(listOf(bubble11, bubble12), repository.getEntities(user11.identifier))
Mockito.verifyNoMoreInteractions(launcherApps)
}
@Test
fun testRemoveBubbles() {
repository.addBubbles(bubbles)
assertEquals(bubbles, repository.bubbles)
repository.addBubbles(user0.identifier, user0bubbles)
repository.addBubbles(user11.identifier, user11bubbles)
repository.removeBubbles(listOf(bubble3))
assertEquals(listOf(bubble1, bubble2), repository.bubbles)
repository.removeBubbles(user0.identifier, listOf(bubble3))
assertEquals(listOf(bubble1, bubble2), repository.getEntities(user0.identifier).toList())
verify(launcherApps).uncacheShortcuts(eq(PKG_MESSENGER),
eq(listOf("shortcut-2")), eq(user0),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
reset(launcherApps)
repository.removeBubbles(user11.identifier, listOf(bubble12))
assertEquals(listOf(bubble11), repository.getEntities(user11.identifier).toList())
verify(launcherApps).uncacheShortcuts(eq(PKG_CHAT),
eq(listOf("alice and bob")), eq(user11),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
}
@Test
fun testAddAndRemoveBubblesWhenExceedingCapacity() {
repository.capacity = 2
// push bubbles beyond capacity
repository.addBubbles(bubbles)
repository.addBubbles(user0.identifier, user0bubbles)
// verify it is trim down to capacity
assertEquals(listOf(bubble2, bubble3), repository.bubbles)
assertEquals(listOf(bubble2, bubble3), repository.getEntities(user0.identifier).toList())
verify(launcherApps).cacheShortcuts(eq(PKG_MESSENGER),
eq(listOf("shortcut-2")), eq(user0),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
verify(launcherApps).cacheShortcuts(eq(PKG_CHAT),
eq(listOf("alice and bob")), eq(user10),
eq(listOf("alice and bob")), eq(user10_managed),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
repository.addBubbles(listOf(bubble1))
// verify the oldest bubble is popped
assertEquals(listOf(bubble3, bubble1), repository.bubbles)
repository.addBubbles(user0.identifier, listOf(bubble1))
// verify the oldest bubble is popped 2, 3
assertEquals(listOf(bubble3, bubble1), repository.getEntities(user0.identifier).toList())
verify(launcherApps).uncacheShortcuts(eq(PKG_CHAT),
eq(listOf("alice and bob")), eq(user10),
eq(listOf("alice and bob")), eq(user10_managed),
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
}
@@ -111,14 +142,14 @@ class BubbleVolatileRepositoryTest : ShellTestCase() {
fun testAddBubbleMatchesByKey() {
val bubble = BubbleEntity(0, "com.example.pkg", "shortcut-id", "key", 120, 0, "title",
1, null)
repository.addBubbles(listOf(bubble))
assertEquals(bubble, repository.bubbles.get(0))
repository.addBubbles(user0.identifier, listOf(bubble))
assertEquals(bubble, repository.getEntities(user0.identifier).get(0))
// Same key as first bubble but different entry
val bubbleModified = BubbleEntity(0, "com.example.pkg", "shortcut-id", "key", 120, 0,
"different title", 2, null)
repository.addBubbles(listOf(bubbleModified))
assertEquals(bubbleModified, repository.bubbles.get(0))
"different title", 2)
repository.addBubbles(user0.identifier, listOf(bubbleModified))
assertEquals(bubbleModified, repository.getEntities(user0.identifier).get(0))
}
}

View File

@@ -18,10 +18,12 @@ package com.android.wm.shell.bubbles.storage
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.testing.AndroidTestingRunner
import android.util.SparseArray
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.ByteArrayInputStream
@@ -31,21 +33,65 @@ import java.io.ByteArrayOutputStream
@RunWith(AndroidTestingRunner::class)
class BubbleXmlHelperTest : ShellTestCase() {
private val bubbles = listOf(
// user, package, shortcut, notification key, height, res-height, title, taskId, locusId
BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0, null, 1),
BubbleEntity(10, "com.example.chat", "alice and bob", "k2", 0, 16537428, "title",
2, null),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3", 120, 0, null,
private val user0Bubbles = listOf(
BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1),
BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2,
null),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null,
INVALID_TASK_ID, "l3")
)
private val user1Bubbles = listOf(
BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3),
BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4,
null),
BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null,
INVALID_TASK_ID, "l4")
)
private val bubbles = SparseArray<List<BubbleEntity>>()
// Checks that the contents of the two sparse arrays are the same.
companion object {
fun sparseArraysEqual(
one: SparseArray<List<BubbleEntity>>?,
two: SparseArray<List<BubbleEntity>>?
): Boolean {
if (one == null && two == null) return true
if ((one == null) != (two == null)) return false
if (one!!.size() != two!!.size()) return false
for (i in 0 until one.size()) {
val k1 = one.keyAt(i)
val v1 = one.valueAt(i)
val k2 = two.keyAt(i)
val v2 = two.valueAt(i)
if (k1 != k2 && v1 != v2) {
return false
}
}
return true
}
}
@Before
fun setup() {
bubbles.put(0, user0Bubbles)
bubbles.put(1, user1Bubbles)
}
@Test
fun testWriteXml() {
val expectedEntries = """
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" tid="1" />
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" tid="2" />
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" tid="-1" l="l3" />
<bs uid="0">
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" />
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" />
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" />
</bs>
<bs uid="1">
<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" />
<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" />
<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" />
</bs>
""".trimIndent()
ByteArrayOutputStream().use {
writeXml(it, bubbles)
@@ -59,19 +105,26 @@ class BubbleXmlHelperTest : ShellTestCase() {
fun testReadXml() {
val src = """
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<bs v="1">
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" tid="1" />
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" tid="2" />
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" tid="-1" l="l3" />
<bs v="2">
<bs uid="0">
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" />
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" />
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" />
</bs>
<bs uid="1">
<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" />
<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" />
<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" />
</bs>
</bs>
""".trimIndent()
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
assertEquals("failed parsing bubbles from xml\n$src", bubbles, actual)
assertTrue("failed parsing bubbles from xml\n$src", sparseArraysEqual(bubbles, actual))
}
// TODO: We should handle upgrades gracefully but this is v1
// V0 -> V1 happened prior to release / during dogfood so nothing is saved
@Test
fun testUpgradeDropsPreviousData() {
fun testUpgradeFromV0DropsPreviousData() {
val src = """
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<bs>
@@ -81,7 +134,7 @@ class BubbleXmlHelperTest : ShellTestCase() {
</bs>
""".trimIndent()
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
assertEquals("failed parsing bubbles from xml\n$src", emptyList<BubbleEntity>(), actual)
assertEquals("failed parsing bubbles from xml\n$src", 0, actual.size())
}
/**
@@ -91,24 +144,25 @@ class BubbleXmlHelperTest : ShellTestCase() {
*/
@Test
fun testReadXMLWithoutTaskId() {
val expectedBubbles = listOf(
BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0, null,
INVALID_TASK_ID),
BubbleEntity(10, "com.example.chat", "alice and bob", "k2", 0, 16537428, "title",
INVALID_TASK_ID),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3", 120, 0, null,
INVALID_TASK_ID)
)
val expectedBubbles = SparseArray<List<BubbleEntity>>()
expectedBubbles.put(0, listOf(
BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0,
null, INVALID_TASK_ID),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3", 120, 0,
null, INVALID_TASK_ID))
)
val src = """
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<bs v="1">
<bs v="2">
<bs uid="0">
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" />
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" />
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" />
</bs>
</bs>
""".trimIndent()
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
assertEquals("failed parsing bubbles from xml\n$src", expectedBubbles, actual)
assertTrue("failed parsing bubbles from xml\n$src",
sparseArraysEqual(expectedBubbles, actual))
}
/**
@@ -117,23 +171,45 @@ class BubbleXmlHelperTest : ShellTestCase() {
*/
@Test
fun testXMLWithoutLocusToLocus() {
val expectedBubbles = listOf(
BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0, null,
INVALID_TASK_ID, null),
BubbleEntity(10, "com.example.chat", "alice and bob", "k2", 0, 16537428, "title",
INVALID_TASK_ID, null),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3", 120, 0, null,
INVALID_TASK_ID, null)
val expectedBubbles = SparseArray<List<BubbleEntity>>()
expectedBubbles.put(0, listOf(
BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0,
null, INVALID_TASK_ID),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3", 120, 0,
null, INVALID_TASK_ID))
)
val src = """
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<bs v="1">
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" />
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" />
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" />
</bs>
""".trimIndent()
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
assertEquals("failed parsing bubbles from xml\n$src", expectedBubbles, actual)
assertTrue("failed parsing bubbles from xml\n$src",
sparseArraysEqual(expectedBubbles, actual))
}
@Test
fun testUpgradeToV2SavesPreviousData() {
val src = """
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<bs v="1">
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" />
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" />
<bb uid="2" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" />
<bb uid="0" pkg="com.example.messenger" sid="shortcut-4" key="k4" h="0" hid="16537428" />
</bs>
""".trimIndent()
val expectedBubbles = SparseArray<List<BubbleEntity>>()
expectedBubbles.put(0, listOf(
BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0,
null, INVALID_TASK_ID, null),
BubbleEntity(0, "com.example.messenger", "shortcut-4", "k4", 0, 16537428,
null, INVALID_TASK_ID, null))
)
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
assertTrue("failed parsing bubbles from xml\n$src",
sparseArraysEqual(expectedBubbles, actual))
}
}

View File

@@ -38,6 +38,7 @@ import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -48,6 +49,7 @@ import android.service.notification.ZenModeConfig;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
import androidx.annotation.NonNull;
@@ -247,6 +249,12 @@ public class BubblesManager implements Dumpable {
public void onUserChanged(int userId) {
mBubbles.onUserChanged(userId);
}
@Override
public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
mBubbles.onCurrentProfilesChanged(currentProfiles);
}
});
mSysuiProxy = new Bubbles.SysuiProxy() {

View File

@@ -236,49 +236,15 @@ public class NotificationTestHelper {
return createRow(PKG, UID, USER_HANDLE, false /* isGroupSummary */, groupkey);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*/
public ExpandableNotificationRow createBubbleInGroup()
throws Exception {
return createBubble(makeBubbleMetadata(null), PKG, true);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*/
public ExpandableNotificationRow createBubble()
throws Exception {
return createBubble(makeBubbleMetadata(null), PKG, false);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*
* @param deleteIntent the intent to assign to {@link BubbleMetadata#deleteIntent}
*/
public ExpandableNotificationRow createBubble(@Nullable PendingIntent deleteIntent)
throws Exception {
return createBubble(makeBubbleMetadata(deleteIntent), PKG, false);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*
* @param bubbleMetadata the {@link BubbleMetadata} to use
*/
public ExpandableNotificationRow createBubble(BubbleMetadata bubbleMetadata, String pkg)
throws Exception {
return createBubble(bubbleMetadata, pkg, false);
}
private ExpandableNotificationRow createBubble(BubbleMetadata bubbleMetadata, String pkg,
boolean inGroup)
throws Exception {
Notification n = createNotification(false /* isGroupSummary */,
inGroup ? GROUP_KEY : null /* groupKey */, bubbleMetadata);
null /* groupKey */, makeBubbleMetadata(null));
n.flags |= FLAG_BUBBLE;
ExpandableNotificationRow row = generateRow(n, pkg, UID, USER_HANDLE,
ExpandableNotificationRow row = generateRow(n, PKG, UID, USER_HANDLE,
0 /* extraInflationFlags */, IMPORTANCE_HIGH);
modifyRanking(row.getEntry())
.setCanBubble(true)
@@ -286,6 +252,76 @@ public class NotificationTestHelper {
return row;
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble and is part
* of a group of notifications.
*/
public ExpandableNotificationRow createBubbleInGroup()
throws Exception {
Notification n = createNotification(false /* isGroupSummary */,
GROUP_KEY /* groupKey */, makeBubbleMetadata(null));
n.flags |= FLAG_BUBBLE;
ExpandableNotificationRow row = generateRow(n, PKG, UID, USER_HANDLE,
0 /* extraInflationFlags */, IMPORTANCE_HIGH);
modifyRanking(row.getEntry())
.setCanBubble(true)
.build();
return row;
}
/**
* Returns an {@link NotificationEntry} that should be shown as a bubble.
*
* @param deleteIntent the intent to assign to {@link BubbleMetadata#deleteIntent}
*/
public NotificationEntry createBubble(@Nullable PendingIntent deleteIntent) {
return createBubble(makeBubbleMetadata(deleteIntent), USER_HANDLE);
}
/**
* Returns an {@link NotificationEntry} that should be shown as a bubble.
*
* @param handle the user to associate with this bubble.
*/
public NotificationEntry createBubble(UserHandle handle) {
return createBubble(makeBubbleMetadata(null), handle);
}
/**
* Returns an {@link NotificationEntry} that should be shown as a bubble.
*
* @param userHandle the user to associate with this notification.
*/
private NotificationEntry createBubble(BubbleMetadata metadata, UserHandle userHandle) {
Notification n = createNotification(false /* isGroupSummary */, null /* groupKey */,
metadata);
n.flags |= FLAG_BUBBLE;
final NotificationChannel channel =
new NotificationChannel(
n.getChannelId(),
n.getChannelId(),
IMPORTANCE_HIGH);
channel.setBlockable(true);
NotificationEntry entry = new NotificationEntryBuilder()
.setPkg(PKG)
.setOpPkg(PKG)
.setId(mId++)
.setUid(UID)
.setInitialPid(2000)
.setNotification(n)
.setUser(userHandle)
.setPostTime(System.currentTimeMillis())
.setChannel(channel)
.build();
modifyRanking(entry)
.setCanBubble(true)
.build();
return entry;
}
/**
* Creates a notification row with the given details.
*

View File

@@ -49,6 +49,7 @@ import android.hardware.display.AmbientDisplayConfiguration;
import android.hardware.face.FaceManager;
import android.os.Handler;
import android.os.PowerManager;
import android.os.UserHandle;
import android.service.dreams.IDreamManager;
import android.service.notification.NotificationListenerService;
import android.service.notification.ZenModeConfig;
@@ -172,14 +173,17 @@ public class BubblesTest extends SysuiTestCase {
private NotificationRemoveInterceptor mRemoveInterceptor;
private NotificationTestHelper mNotificationTestHelper;
private ExpandableNotificationRow mRow;
private ExpandableNotificationRow mRow2;
private ExpandableNotificationRow mRow3;
private NotificationEntry mRow;
private NotificationEntry mRow2;
private NotificationEntry mRow3;
private ExpandableNotificationRow mNonBubbleNotifRow;
private BubbleEntry mBubbleEntry;
private BubbleEntry mBubbleEntry2;
private BubbleEntry mBubbleEntry3;
private BubbleEntry mBubbleEntryUser11;
private BubbleEntry mBubbleEntry2User11;
@Mock
private Bubbles.BubbleExpandListener mBubbleExpandListener;
@Mock
@@ -245,14 +249,20 @@ public class BubblesTest extends SysuiTestCase {
mRow2 = mNotificationTestHelper.createBubble(mDeleteIntent);
mRow3 = mNotificationTestHelper.createBubble(mDeleteIntent);
mNonBubbleNotifRow = mNotificationTestHelper.createRow();
mBubbleEntry = BubblesManager.notifToBubbleEntry(mRow.getEntry());
mBubbleEntry2 = BubblesManager.notifToBubbleEntry(mRow2.getEntry());
mBubbleEntry3 = BubblesManager.notifToBubbleEntry(mRow3.getEntry());
mBubbleEntry = BubblesManager.notifToBubbleEntry(mRow);
mBubbleEntry2 = BubblesManager.notifToBubbleEntry(mRow2);
mBubbleEntry3 = BubblesManager.notifToBubbleEntry(mRow3);
UserHandle handle = mock(UserHandle.class);
when(handle.getIdentifier()).thenReturn(11);
mBubbleEntryUser11 = BubblesManager.notifToBubbleEntry(
mNotificationTestHelper.createBubble(handle));
mBubbleEntry2User11 = BubblesManager.notifToBubbleEntry(
mNotificationTestHelper.createBubble(handle));
// Return non-null notification data from the NEM
when(mNotificationEntryManager
.getActiveNotificationUnfiltered(mRow.getEntry().getKey())).thenReturn(
mRow.getEntry());
.getActiveNotificationUnfiltered(mRow.getKey())).thenReturn(mRow);
mZenModeConfig.suppressedVisualEffects = 0;
when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);
@@ -351,8 +361,8 @@ public class BubblesTest extends SysuiTestCase {
verify(mNotificationEntryManager).updateNotifications(any());
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
verify(mNotificationEntryManager, times(2)).updateNotifications(anyString());
assertFalse(mSysUiStateBubblesExpanded);
@@ -362,20 +372,20 @@ public class BubblesTest extends SysuiTestCase {
public void testPromoteBubble_autoExpand() throws Exception {
mBubbleController.updateBubble(mBubbleEntry2);
mBubbleController.updateBubble(mBubbleEntry);
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getEntry().getKey()))
.thenReturn(mRow.getEntry());
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow2.getEntry().getKey()))
.thenReturn(mRow2.getEntry());
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getKey()))
.thenReturn(mRow);
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow2.getKey()))
.thenReturn(mRow2);
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
Bubble b = mBubbleData.getOverflowBubbleWithKey(mRow.getEntry().getKey());
Bubble b = mBubbleData.getOverflowBubbleWithKey(mRow.getKey());
assertThat(mBubbleData.getOverflowBubbles()).isEqualTo(ImmutableList.of(b));
verify(mNotificationEntryManager, never()).performRemoveNotification(
eq(mRow.getEntry().getSbn()), any(), anyInt());
assertThat(mRow.getEntry().isBubble()).isFalse();
eq(mRow.getSbn()), any(), anyInt());
assertThat(mRow.isBubble()).isFalse();
Bubble b2 = mBubbleData.getBubbleInStackWithKey(mRow2.getEntry().getKey());
Bubble b2 = mBubbleData.getBubbleInStackWithKey(mRow2.getKey());
assertThat(mBubbleData.getSelectedBubble()).isEqualTo(b2);
mBubbleController.promoteBubbleFromOverflow(b);
@@ -393,19 +403,19 @@ public class BubblesTest extends SysuiTestCase {
mBubbleController.updateBubble(mBubbleEntry2);
mBubbleController.updateBubble(mBubbleEntry, /* suppressFlyout */
false, /* showInShade */ true);
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getEntry().getKey()))
.thenReturn(mRow.getEntry());
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow2.getEntry().getKey()))
.thenReturn(mRow2.getEntry());
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getKey()))
.thenReturn(mRow);
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow2.getKey()))
.thenReturn(mRow2);
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
mRow.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
verify(mNotificationEntryManager, times(1)).performRemoveNotification(
eq(mRow.getEntry().getSbn()), any(), anyInt());
eq(mRow.getSbn()), any(), anyInt());
assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
assertFalse(mRow.getEntry().isBubble());
assertFalse(mRow.isBubble());
}
@Test
@@ -414,28 +424,28 @@ public class BubblesTest extends SysuiTestCase {
assertTrue(mBubbleController.hasBubbles());
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_CHANGED);
mRow.getKey(), Bubbles.DISMISS_USER_CHANGED);
verify(mNotificationEntryManager, never()).performRemoveNotification(
eq(mRow.getEntry().getSbn()), any(), anyInt());
eq(mRow.getSbn()), any(), anyInt());
assertFalse(mBubbleController.hasBubbles());
assertFalse(mSysUiStateBubblesExpanded);
assertTrue(mRow.getEntry().isBubble());
assertTrue(mRow.isBubble());
}
@Test
public void testDismissStack() {
mBubbleController.updateBubble(mBubbleEntry);
verify(mNotificationEntryManager, times(1)).updateNotifications(any());
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
mBubbleController.updateBubble(mBubbleEntry2);
verify(mNotificationEntryManager, times(2)).updateNotifications(any());
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow2.getEntry().getKey()));
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey()));
assertTrue(mBubbleController.hasBubbles());
mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
verify(mNotificationEntryManager, times(3)).updateNotifications(any());
assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
assertNull(mBubbleData.getBubbleInStackWithKey(mRow2.getEntry().getKey()));
assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
assertNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey()));
assertFalse(mSysUiStateBubblesExpanded);
}
@@ -445,7 +455,7 @@ public class BubblesTest extends SysuiTestCase {
assertFalse(mBubbleController.isStackExpanded());
// Mark it as a bubble and add it explicitly
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// We should have bubbles & their notifs should not be suppressed
@@ -457,7 +467,7 @@ public class BubblesTest extends SysuiTestCase {
BubbleStackView stackView = mBubbleController.getStackView();
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
assertTrue(mSysUiStateBubblesExpanded);
@@ -467,7 +477,7 @@ public class BubblesTest extends SysuiTestCase {
// Collapse
mBubbleController.collapseStack();
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
assertFalse(mBubbleController.isStackExpanded());
assertFalse(mSysUiStateBubblesExpanded);
@@ -477,8 +487,8 @@ public class BubblesTest extends SysuiTestCase {
@Ignore("Currently broken.")
public void testCollapseAfterChangingExpandedBubble() {
// Mark it as a bubble and add it explicitly
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow2.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mEntryListener.onPendingEntryAdded(mRow2);
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.updateBubble(mBubbleEntry2);
@@ -494,30 +504,30 @@ public class BubblesTest extends SysuiTestCase {
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
true, mRow2.getEntry().getKey());
true, mRow2.getKey());
assertTrue(mSysUiStateBubblesExpanded);
// Last added is the one that is expanded
assertEquals(mRow2.getEntry().getKey(), mBubbleData.getSelectedBubble().getKey());
assertEquals(mRow2.getKey(), mBubbleData.getSelectedBubble().getKey());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry2.getKey(), mBubbleEntry2.getGroupKey()));
// Switch which bubble is expanded
mBubbleData.setSelectedBubble(mBubbleData.getBubbleInStackWithKey(
mRow.getEntry().getKey()));
mRow.getKey()));
mBubbleData.setExpanded(true);
assertEquals(mRow.getEntry().getKey(), mBubbleData.getBubbleInStackWithKey(
assertEquals(mRow.getKey(), mBubbleData.getBubbleInStackWithKey(
stackView.getExpandedBubble().getKey()).getKey());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// collapse for previous bubble
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
false, mRow2.getEntry().getKey());
false, mRow2.getKey());
// expand for selected bubble
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
true, mRow.getEntry().getKey());
true, mRow.getKey());
// Collapse
mBubbleController.collapseStack();
@@ -529,7 +539,7 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testExpansionRemovesShowInShadeAndDot() {
// Mark it as a bubble and add it explicitly
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// We should have bubbles & their notifs should not be suppressed
@@ -538,12 +548,12 @@ public class BubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mTestableLooper.processAllMessages();
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
// Expand
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
assertTrue(mSysUiStateBubblesExpanded);
@@ -551,13 +561,13 @@ public class BubblesTest extends SysuiTestCase {
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Notif shouldn't show dot after expansion
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
}
@Test
public void testUpdateWhileExpanded_DoesntChangeShowInShadeAndDot() {
// Mark it as a bubble and add it explicitly
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// We should have bubbles & their notifs should not be suppressed
@@ -566,12 +576,12 @@ public class BubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mTestableLooper.processAllMessages();
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
// Expand
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
assertTrue(mSysUiStateBubblesExpanded);
@@ -579,24 +589,24 @@ public class BubblesTest extends SysuiTestCase {
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Notif shouldn't show dot after expansion
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
// Send update
mEntryListener.onPreEntryUpdated(mRow.getEntry());
mEntryListener.onPreEntryUpdated(mRow);
// Nothing should have changed
// Notif is suppressed after expansion
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Notif shouldn't show dot after expansion
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
}
@Test
public void testRemoveLastExpanded_selectsOverflow() {
// Mark it as a bubble and add it explicitly
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow2.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mEntryListener.onPendingEntryAdded(mRow2);
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.updateBubble(mBubbleEntry2);
@@ -607,10 +617,10 @@ public class BubblesTest extends SysuiTestCase {
assertTrue(mSysUiStateBubblesExpanded);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getKey());
// Last added is the one that is expanded
assertEquals(mRow2.getEntry().getKey(), mBubbleData.getBubbleInStackWithKey(
assertEquals(mRow2.getKey(), mBubbleData.getBubbleInStackWithKey(
stackView.getExpandedBubble().getKey()).getKey());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry2.getKey(), mBubbleEntry2.getGroupKey()));
@@ -620,12 +630,12 @@ public class BubblesTest extends SysuiTestCase {
mBubbleData.getBubbleInStackWithKey(
stackView.getExpandedBubble().getKey()).getKey(),
Bubbles.DISMISS_USER_GESTURE);
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getKey());
// Make sure first bubble is selected
assertEquals(mRow.getEntry().getKey(), mBubbleData.getBubbleInStackWithKey(
assertEquals(mRow.getKey(), mBubbleData.getBubbleInStackWithKey(
stackView.getExpandedBubble().getKey()).getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
// Dismiss that one
mBubbleController.removeBubble(
@@ -643,7 +653,7 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testRemoveLastExpandedEmptyOverflow_collapses() {
// Mark it as a bubble and add it explicitly
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Expand
@@ -652,7 +662,7 @@ public class BubblesTest extends SysuiTestCase {
assertTrue(mSysUiStateBubblesExpanded);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
// Block the bubble so it won't be in the overflow
mBubbleController.removeBubble(
@@ -660,10 +670,10 @@ public class BubblesTest extends SysuiTestCase {
stackView.getExpandedBubble().getKey()).getKey(),
Bubbles.DISMISS_BLOCKED);
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
// We should be collapsed
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
assertFalse(mBubbleController.hasBubbles());
assertFalse(mSysUiStateBubblesExpanded);
}
@@ -671,16 +681,16 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testAutoExpand_fails_noFlag() {
assertFalse(mBubbleController.isStackExpanded());
setMetadataFlags(mRow.getEntry(),
setMetadataFlags(mRow,
Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, false /* enableFlag */);
// Add the auto expand bubble
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Expansion shouldn't change
verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */,
mRow.getEntry().getKey());
mRow.getKey());
assertFalse(mBubbleController.isStackExpanded());
assertFalse(mSysUiStateBubblesExpanded);
@@ -688,16 +698,16 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testAutoExpand_succeeds_withFlag() {
setMetadataFlags(mRow.getEntry(),
setMetadataFlags(mRow,
Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, true /* enableFlag */);
// Add the auto expand bubble
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Expansion should change
verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */,
mRow.getEntry().getKey());
mRow.getKey());
assertTrue(mBubbleController.isStackExpanded());
assertTrue(mSysUiStateBubblesExpanded);
@@ -705,19 +715,19 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testSuppressNotif_onInitialNotif() {
setMetadataFlags(mRow.getEntry(),
setMetadataFlags(mRow,
Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */);
// Add the suppress notif bubble
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Notif should be suppressed because we were foreground
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Dot + flyout is hidden because notif is suppressed
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showFlyout());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
assertFalse(mSysUiStateBubblesExpanded);
}
@@ -730,10 +740,10 @@ public class BubblesTest extends SysuiTestCase {
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Should show dot
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
// Update to suppress notif
setMetadataFlags(mRow.getEntry(),
setMetadataFlags(mRow,
Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */);
mBubbleController.updateBubble(mBubbleEntry);
@@ -741,22 +751,20 @@ public class BubblesTest extends SysuiTestCase {
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Dot + flyout is hidden because notif is suppressed
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showFlyout());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
assertFalse(mSysUiStateBubblesExpanded);
}
@Test
public void testExpandStackAndSelectBubble_removedFirst() {
final String key = mRow.getEntry().getKey();
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Simulate notification cancellation.
mRemoveInterceptor.onNotificationRemoveRequested(
mRow.getEntry().getKey(), mRow.getEntry(), REASON_APP_CANCEL);
mRow.getKey(), mRow, REASON_APP_CANCEL);
mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
@@ -765,12 +773,12 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testMarkNewNotificationAsShowInShade() {
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mTestableLooper.processAllMessages();
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
}
@Test
@@ -784,7 +792,7 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.removeBubble(mRow.getEntry().getKey(), Bubbles.DISMISS_AGED);
mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_AGED);
verify(mDeleteIntent, never()).send();
}
@@ -792,7 +800,7 @@ public class BubblesTest extends SysuiTestCase {
public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
verify(mDeleteIntent, times(1)).send();
}
@@ -810,11 +818,11 @@ public class BubblesTest extends SysuiTestCase {
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
mRow.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
mRow.getSbn().getNotification().flags &= ~FLAG_BUBBLE;
NotificationListenerService.Ranking ranking = new RankingBuilder(
mRow.getEntry().getRanking()).setCanBubble(false).build();
mRow.getEntry().setRanking(ranking);
mEntryListener.onPreEntryUpdated(mRow.getEntry());
mRow.getRanking()).setCanBubble(false).build();
mRow.setRanking(ranking);
mEntryListener.onPreEntryUpdated(mRow);
assertFalse(mBubbleController.hasBubbles());
verify(mDeleteIntent, never()).send();
@@ -822,13 +830,13 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testRemoveBubble_succeeds_appCancel() {
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
mRow.getEntry().getKey(), mRow.getEntry(), REASON_APP_CANCEL);
mRow.getKey(), mRow, REASON_APP_CANCEL);
// Cancels always remove so no need to intercept
assertFalse(intercepted);
@@ -836,19 +844,19 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void testRemoveBubble_entryListenerRemove() {
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
// Removes the notification
mEntryListener.onEntryRemoved(mRow.getEntry(), null, false, REASON_APP_CANCEL);
mEntryListener.onEntryRemoved(mRow, null, false, REASON_APP_CANCEL);
assertFalse(mBubbleController.hasBubbles());
}
@Test
public void removeBubble_clearAllIntercepted() {
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
@@ -856,7 +864,7 @@ public class BubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL_ALL);
mRow.getKey(), mRow, REASON_CANCEL_ALL);
// Intercept!
assertTrue(intercepted);
@@ -867,7 +875,7 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void removeBubble_userDismissNotifIntercepted() {
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
@@ -875,7 +883,7 @@ public class BubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL);
mRow.getKey(), mRow, REASON_CANCEL);
// Intercept!
assertTrue(intercepted);
@@ -887,7 +895,7 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void removeNotif_inOverflow_intercepted() {
// Get bubble with notif in shade.
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
@@ -895,11 +903,11 @@ public class BubblesTest extends SysuiTestCase {
// Dismiss the bubble into overflow.
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
assertFalse(mBubbleController.hasBubbles());
boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL);
mRow.getKey(), mRow, REASON_CANCEL);
// Notif is no longer a bubble, but still in overflow, so we intercept removal.
assertTrue(intercepted);
@@ -908,7 +916,7 @@ public class BubblesTest extends SysuiTestCase {
@Test
public void removeNotif_notInOverflow_notIntercepted() {
// Get bubble with notif in shade.
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
@@ -916,11 +924,11 @@ public class BubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_NO_LONGER_BUBBLE);
mRow.getKey(), Bubbles.DISMISS_NO_LONGER_BUBBLE);
assertFalse(mBubbleController.hasBubbles());
boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL);
mRow.getKey(), mRow, REASON_CANCEL);
// Notif is no longer a bubble, so we should not intercept removal.
assertFalse(intercepted);
@@ -934,25 +942,25 @@ public class BubblesTest extends SysuiTestCase {
mBubbleEntry2, /* suppressFlyout */ false, /* showInShade */ false);
mBubbleController.updateBubble(
mBubbleEntry3, /* suppressFlyout */ false, /* showInShade */ false);
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getEntry().getKey()))
.thenReturn(mRow.getEntry());
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow2.getEntry().getKey()))
.thenReturn(mRow2.getEntry());
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow3.getEntry().getKey()))
.thenReturn(mRow3.getEntry());
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getKey()))
.thenReturn(mRow);
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow2.getKey()))
.thenReturn(mRow2);
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow3.getKey()))
.thenReturn(mRow3);
assertEquals(mBubbleData.getBubbles().size(), 3);
mBubbleData.setMaxOverflowBubbles(1);
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
assertEquals(mBubbleData.getBubbles().size(), 2);
assertEquals(mBubbleData.getOverflowBubbles().size(), 1);
mBubbleController.removeBubble(
mRow2.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
mRow2.getKey(), Bubbles.DISMISS_USER_GESTURE);
// Overflow max of 1 is reached; mRow is oldest, so it gets removed
verify(mNotificationEntryManager, times(1)).performRemoveNotification(
eq(mRow.getEntry().getSbn()), any(), eq(REASON_CANCEL));
eq(mRow.getSbn()), any(), eq(REASON_CANCEL));
assertEquals(mBubbleData.getBubbles().size(), 1);
assertEquals(mBubbleData.getOverflowBubbles().size(), 1);
}
@@ -963,14 +971,14 @@ public class BubblesTest extends SysuiTestCase {
mock(Bubbles.SuppressionChangedListener.class);
mBubbleData.setSuppressionChangedListener(listener);
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mRemoveInterceptor.onNotificationRemoveRequested(
mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL);
mRow.getKey(), mRow, REASON_CANCEL);
// Should update show in shade state
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
@@ -978,7 +986,7 @@ public class BubblesTest extends SysuiTestCase {
// Should notify delegate that shade state changed
verify(listener).onBubbleNotificationSuppressionChange(
mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
}
@Test
@@ -987,7 +995,7 @@ public class BubblesTest extends SysuiTestCase {
mock(Bubbles.SuppressionChangedListener.class);
mBubbleData.setSuppressionChangedListener(listener);
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mEntryListener.onPendingEntryAdded(mRow);
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
@@ -1001,7 +1009,7 @@ public class BubblesTest extends SysuiTestCase {
// Should notify delegate that shade state changed
verify(listener).onBubbleNotificationSuppressionChange(
mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
}
@Test
@@ -1094,14 +1102,47 @@ public class BubblesTest extends SysuiTestCase {
assertTrue(mBubbleController.hasBubbles());
// Overflow it
mBubbleData.dismissBubbleWithKey(mRow.getEntry().getKey(),
mBubbleData.dismissBubbleWithKey(mRow.getKey(),
Bubbles.DISMISS_USER_GESTURE);
assertThat(mBubbleData.hasBubbleInStackWithKey(mRow.getEntry().getKey())).isFalse();
assertThat(mBubbleData.hasOverflowBubbleWithKey(mRow.getEntry().getKey())).isTrue();
assertThat(mBubbleData.hasBubbleInStackWithKey(mRow.getKey())).isFalse();
assertThat(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey())).isTrue();
// Test
mBubbleController.updateBubble(mBubbleEntry);
assertThat(mBubbleData.hasBubbleInStackWithKey(mRow.getEntry().getKey())).isFalse();
assertThat(mBubbleData.hasBubbleInStackWithKey(mRow.getKey())).isFalse();
}
/**
* Verifies that when the user changes, the bubbles in the overflow list is cleared. Doesn't
* test the loading from the repository which would be a nice thing to add.
*/
@Test
public void testOnUserChanged_overflowState() {
int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier();
int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier();
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.updateBubble(mBubbleEntry2);
assertTrue(mBubbleController.hasBubbles());
mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
// Verify these are in the overflow
assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey())).isNotNull();
assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2.getKey())).isNotNull();
// Switch users
mBubbleController.onUserChanged(secondUserId);
assertThat(mBubbleData.getOverflowBubbles().isEmpty());
// Give this user some bubbles
mBubbleController.updateBubble(mBubbleEntryUser11);
mBubbleController.updateBubble(mBubbleEntry2User11);
assertTrue(mBubbleController.hasBubbles());
mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
// Verify these are in the overflow
assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntryUser11.getKey())).isNotNull();
assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2User11.getKey())).isNotNull();
}
/**

View File

@@ -158,8 +158,8 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
private NotifCollectionListener mEntryListener;
private NotificationTestHelper mNotificationTestHelper;
private ExpandableNotificationRow mRow;
private ExpandableNotificationRow mRow2;
private NotificationEntry mRow;
private NotificationEntry mRow2;
private ExpandableNotificationRow mNonBubbleNotifRow;
private BubbleEntry mBubbleEntry;
private BubbleEntry mBubbleEntry2;
@@ -224,8 +224,8 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mRow = mNotificationTestHelper.createBubble(mDeleteIntent);
mRow2 = mNotificationTestHelper.createBubble(mDeleteIntent);
mNonBubbleNotifRow = mNotificationTestHelper.createRow();
mBubbleEntry = BubblesManager.notifToBubbleEntry(mRow.getEntry());
mBubbleEntry2 = BubblesManager.notifToBubbleEntry(mRow2.getEntry());
mBubbleEntry = BubblesManager.notifToBubbleEntry(mRow);
mBubbleEntry2 = BubblesManager.notifToBubbleEntry(mRow2);
mZenModeConfig.suppressedVisualEffects = 0;
when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);
@@ -306,19 +306,19 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Test
public void testRemoveBubble() {
mBubbleController.updateBubble(mBubbleEntry);
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
assertTrue(mBubbleController.hasBubbles());
verify(mNotifCallback, times(1)).invalidateNotifications(anyString());
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
verify(mNotifCallback, times(2)).invalidateNotifications(anyString());
}
@Test
public void testRemoveBubble_withDismissedNotif_inOverflow() {
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
@@ -326,40 +326,40 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Make it look like dismissed notif
mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).setSuppressNotification(true);
mBubbleData.getBubbleInStackWithKey(mRow.getKey()).setSuppressNotification(true);
// Now remove the bubble
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
assertTrue(mBubbleData.hasOverflowBubbleWithKey(mRow.getEntry().getKey()));
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
assertTrue(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey()));
// We don't remove the notification since the bubble is still in overflow.
verify(mNotifCallback, never()).removeNotification(eq(mRow.getEntry()), any(), anyInt());
verify(mNotifCallback, never()).removeNotification(eq(mRow), any(), anyInt());
assertFalse(mBubbleController.hasBubbles());
}
@Test
public void testRemoveBubble_withDismissedNotif_notInOverflow() {
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getEntry().getKey()))
.thenReturn(mRow.getEntry());
when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getKey()))
.thenReturn(mRow);
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Make it look like dismissed notif
mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).setSuppressNotification(true);
mBubbleData.getBubbleInStackWithKey(mRow.getKey()).setSuppressNotification(true);
// Now remove the bubble
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
assertFalse(mBubbleData.hasOverflowBubbleWithKey(mRow.getEntry().getKey()));
mRow.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
assertFalse(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey()));
// Since the notif is dismissed and not in overflow, once the bubble is removed,
// removeNotification gets called to really remove the notif
verify(mNotifCallback, times(1)).removeNotification(eq(mRow.getEntry()),
verify(mNotifCallback, times(1)).removeNotification(eq(mRow),
any(), anyInt());
assertFalse(mBubbleController.hasBubbles());
}
@@ -368,16 +368,16 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
public void testDismissStack() {
mBubbleController.updateBubble(mBubbleEntry);
verify(mNotifCallback, times(1)).invalidateNotifications(anyString());
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
mBubbleController.updateBubble(mBubbleEntry2);
verify(mNotifCallback, times(2)).invalidateNotifications(anyString());
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow2.getEntry().getKey()));
assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey()));
assertTrue(mBubbleController.hasBubbles());
mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
verify(mNotifCallback, times(3)).invalidateNotifications(anyString());
assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
assertNull(mBubbleData.getBubbleInStackWithKey(mRow2.getEntry().getKey()));
assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
assertNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey()));
}
@Test
@@ -385,7 +385,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
assertFalse(mBubbleController.isStackExpanded());
// Mark it as a bubble and add it explicitly
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// We should have bubbles & their notifs should not be suppressed
@@ -397,7 +397,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
BubbleStackView stackView = mBubbleController.getStackView();
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
// Make sure the notif is suppressed
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
@@ -405,7 +405,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
// Collapse
mBubbleController.collapseStack();
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
assertFalse(mBubbleController.isStackExpanded());
}
@@ -413,8 +413,8 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Ignore("Currently broken.")
public void testCollapseAfterChangingExpandedBubble() {
// Mark it as a bubble and add it explicitly
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow2.getEntry());
mEntryListener.onEntryAdded(mRow);
mEntryListener.onEntryAdded(mRow2);
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.updateBubble(mBubbleEntry2);
@@ -430,28 +430,28 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
true, mRow2.getEntry().getKey());
true, mRow2.getKey());
// Last added is the one that is expanded
assertEquals(mRow2.getEntry().getKey(), mBubbleData.getSelectedBubble().getKey());
assertEquals(mRow2.getKey(), mBubbleData.getSelectedBubble().getKey());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry2.getKey(), mBubbleEntry2.getGroupKey()));
// Switch which bubble is expanded
mBubbleData.setSelectedBubble(mBubbleData.getBubbleInStackWithKey(
mRow.getEntry().getKey()));
mRow.getKey()));
mBubbleData.setExpanded(true);
assertEquals(mRow.getEntry().getKey(), mBubbleData.getBubbleInStackWithKey(
assertEquals(mRow.getKey(), mBubbleData.getBubbleInStackWithKey(
stackView.getExpandedBubble().getKey()).getKey());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// collapse for previous bubble
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
false, mRow2.getEntry().getKey());
false, mRow2.getKey());
// expand for selected bubble
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
true, mRow.getEntry().getKey());
true, mRow.getKey());
// Collapse
@@ -462,7 +462,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Test
public void testExpansionRemovesShowInShadeAndDot() {
// Mark it as a bubble and add it explicitly
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// We should have bubbles & their notifs should not be suppressed
@@ -471,24 +471,24 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mTestableLooper.processAllMessages();
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
// Expand
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
// Notif is suppressed after expansion
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Notif shouldn't show dot after expansion
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
}
@Test
public void testUpdateWhileExpanded_DoesntChangeShowInShadeAndDot() {
// Mark it as a bubble and add it explicitly
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// We should have bubbles & their notifs should not be suppressed
@@ -497,35 +497,35 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mTestableLooper.processAllMessages();
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
// Expand
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
// Notif is suppressed after expansion
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Notif shouldn't show dot after expansion
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
// Send update
mEntryListener.onEntryUpdated(mRow.getEntry());
mEntryListener.onEntryUpdated(mRow);
// Nothing should have changed
// Notif is suppressed after expansion
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Notif shouldn't show dot after expansion
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
}
@Test
public void testRemoveLastExpanded_selectsOverflow() {
// Mark it as a bubble and add it explicitly
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow2.getEntry());
mEntryListener.onEntryAdded(mRow);
mEntryListener.onEntryAdded(mRow2);
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.updateBubble(mBubbleEntry2);
@@ -534,10 +534,10 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getKey());
// Last added is the one that is expanded
assertEquals(mRow2.getEntry().getKey(), mBubbleData.getBubbleInStackWithKey(
assertEquals(mRow2.getKey(), mBubbleData.getBubbleInStackWithKey(
stackView.getExpandedBubble().getKey()).getKey());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry2.getKey(), mBubbleEntry2.getGroupKey()));
@@ -547,12 +547,12 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleData.getBubbleInStackWithKey(
stackView.getExpandedBubble().getKey()).getKey(),
Bubbles.DISMISS_USER_GESTURE);
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getKey());
// Make sure first bubble is selected
assertEquals(mRow.getEntry().getKey(), mBubbleData.getBubbleInStackWithKey(
assertEquals(mRow.getKey(), mBubbleData.getBubbleInStackWithKey(
stackView.getExpandedBubble().getKey()).getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
// Dismiss that one
mBubbleController.removeBubble(
@@ -569,7 +569,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Test
public void testRemoveLastExpandedEmptyOverflow_collapses() {
// Mark it as a bubble and add it explicitly
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Expand
@@ -577,7 +577,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
// Block the bubble so it won't be in the overflow
mBubbleController.removeBubble(
@@ -585,10 +585,10 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
stackView.getExpandedBubble().getKey()).getKey(),
Bubbles.DISMISS_BLOCKED);
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
// We should be collapsed
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().getKey());
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
assertFalse(mBubbleController.hasBubbles());
}
@@ -596,49 +596,49 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Test
public void testAutoExpand_fails_noFlag() {
assertFalse(mBubbleController.isStackExpanded());
setMetadataFlags(mRow.getEntry(),
setMetadataFlags(mRow,
Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, false /* enableFlag */);
// Add the auto expand bubble
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Expansion shouldn't change
verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */,
mRow.getEntry().getKey());
mRow.getKey());
assertFalse(mBubbleController.isStackExpanded());
}
@Test
public void testAutoExpand_succeeds_withFlag() {
setMetadataFlags(mRow.getEntry(),
setMetadataFlags(mRow,
Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, true /* enableFlag */);
// Add the auto expand bubble
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Expansion should change
verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */,
mRow.getEntry().getKey());
mRow.getKey());
assertTrue(mBubbleController.isStackExpanded());
}
@Test
public void testSuppressNotif_onInitialNotif() {
setMetadataFlags(mRow.getEntry(),
setMetadataFlags(mRow,
Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */);
// Add the suppress notif bubble
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
// Notif should be suppressed because we were foreground
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Dot + flyout is hidden because notif is suppressed
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showFlyout());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
}
@Test
@@ -649,10 +649,10 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Should show dot
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
// Update to suppress notif
setMetadataFlags(mRow.getEntry(),
setMetadataFlags(mRow,
Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */);
mBubbleController.updateBubble(mBubbleEntry);
@@ -660,18 +660,18 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Dot + flyout is hidden because notif is suppressed
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showFlyout());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
}
@Test
public void testMarkNewNotificationAsShowInShade() {
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mTestableLooper.processAllMessages();
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()).showDot());
assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
}
@Test
@@ -685,7 +685,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Test
public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.removeBubble(mRow.getEntry().getKey(), Bubbles.DISMISS_AGED);
mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_AGED);
verify(mDeleteIntent, never()).send();
}
@@ -693,7 +693,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.removeBubble(
mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
verify(mDeleteIntent, times(1)).send();
}
@@ -711,11 +711,11 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
mRow.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
mRow.getSbn().getNotification().flags &= ~FLAG_BUBBLE;
NotificationListenerService.Ranking ranking = new RankingBuilder(
mRow.getEntry().getRanking()).setCanBubble(false).build();
mRow.getEntry().setRanking(ranking);
mEntryListener.onEntryUpdated(mRow.getEntry());
mRow.getRanking()).setCanBubble(false).build();
mRow.setRanking(ranking);
mEntryListener.onEntryUpdated(mRow);
assertFalse(mBubbleController.hasBubbles());
verify(mDeleteIntent, never()).send();
@@ -723,26 +723,26 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Test
public void testRemoveBubble_entryListenerRemove() {
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
// Removes the notification
mEntryListener.onEntryRemoved(mRow.getEntry(), 0);
mEntryListener.onEntryRemoved(mRow, 0);
assertFalse(mBubbleController.hasBubbles());
}
@Test
public void removeBubble_intercepted() {
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
boolean intercepted = mBubblesManager.handleDismissalInterception(mRow.getEntry());
boolean intercepted = mBubblesManager.handleDismissalInterception(mRow);
// Intercept!
assertTrue(intercepted);
@@ -753,7 +753,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Test
public void removeBubble_dismissIntoOverflow_intercepted() {
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
@@ -761,11 +761,11 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Dismiss the bubble
mBubbleController.removeBubble(mRow.getEntry().getKey(), Bubbles.DISMISS_USER_GESTURE);
mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
assertFalse(mBubbleController.hasBubbles());
// Dismiss the notification
boolean intercepted = mBubblesManager.handleDismissalInterception(mRow.getEntry());
boolean intercepted = mBubblesManager.handleDismissalInterception(mRow);
// Intercept dismissal since bubble is going into overflow
assertTrue(intercepted);
@@ -773,7 +773,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
@Test
public void removeBubble_notIntercepted() {
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
assertTrue(mBubbleController.hasBubbles());
@@ -781,11 +781,11 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
// Dismiss the bubble
mBubbleController.removeBubble(mRow.getEntry().getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
assertFalse(mBubbleController.hasBubbles());
// Dismiss the notification
boolean intercepted = mBubblesManager.handleDismissalInterception(mRow.getEntry());
boolean intercepted = mBubblesManager.handleDismissalInterception(mRow);
// Not a bubble anymore so we don't intercept dismissal.
assertFalse(intercepted);
@@ -797,13 +797,13 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mock(Bubbles.SuppressionChangedListener.class);
mBubbleData.setSuppressionChangedListener(listener);
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mBubbleEntry.getKey(), mBubbleEntry.getGroupKey()));
mBubblesManager.handleDismissalInterception(mRow.getEntry());
mBubblesManager.handleDismissalInterception(mRow);
// Should update show in shade state
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
@@ -811,7 +811,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
// Should notify delegate that shade state changed
verify(listener).onBubbleNotificationSuppressionChange(
mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
}
@Test
@@ -820,7 +820,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mock(Bubbles.SuppressionChangedListener.class);
mBubbleData.setSuppressionChangedListener(listener);
mEntryListener.onEntryAdded(mRow.getEntry());
mEntryListener.onEntryAdded(mRow);
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
@@ -834,7 +834,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
// Should notify delegate that shade state changed
verify(listener).onBubbleNotificationSuppressionChange(
mBubbleData.getBubbleInStackWithKey(mRow.getEntry().getKey()));
mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
}
@Test