Merge "When a user is removed make sure we remove their persisted bubbles" into tm-dev
This commit is contained in:
@@ -66,6 +66,7 @@ import android.os.Handler;
|
|||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.os.ServiceManager;
|
import android.os.ServiceManager;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
|
import android.os.UserManager;
|
||||||
import android.service.notification.NotificationListenerService;
|
import android.service.notification.NotificationListenerService;
|
||||||
import android.service.notification.NotificationListenerService.RankingMap;
|
import android.service.notification.NotificationListenerService.RankingMap;
|
||||||
import android.util.ArraySet;
|
import android.util.ArraySet;
|
||||||
@@ -147,6 +148,7 @@ public class BubbleController {
|
|||||||
private final FloatingContentCoordinator mFloatingContentCoordinator;
|
private final FloatingContentCoordinator mFloatingContentCoordinator;
|
||||||
private final BubbleDataRepository mDataRepository;
|
private final BubbleDataRepository mDataRepository;
|
||||||
private final WindowManagerShellWrapper mWindowManagerShellWrapper;
|
private final WindowManagerShellWrapper mWindowManagerShellWrapper;
|
||||||
|
private final UserManager mUserManager;
|
||||||
private final LauncherApps mLauncherApps;
|
private final LauncherApps mLauncherApps;
|
||||||
private final IStatusBarService mBarService;
|
private final IStatusBarService mBarService;
|
||||||
private final WindowManager mWindowManager;
|
private final WindowManager mWindowManager;
|
||||||
@@ -231,6 +233,7 @@ public class BubbleController {
|
|||||||
@Nullable IStatusBarService statusBarService,
|
@Nullable IStatusBarService statusBarService,
|
||||||
WindowManager windowManager,
|
WindowManager windowManager,
|
||||||
WindowManagerShellWrapper windowManagerShellWrapper,
|
WindowManagerShellWrapper windowManagerShellWrapper,
|
||||||
|
UserManager userManager,
|
||||||
LauncherApps launcherApps,
|
LauncherApps launcherApps,
|
||||||
TaskStackListenerImpl taskStackListener,
|
TaskStackListenerImpl taskStackListener,
|
||||||
UiEventLogger uiEventLogger,
|
UiEventLogger uiEventLogger,
|
||||||
@@ -248,8 +251,8 @@ public class BubbleController {
|
|||||||
BubbleData data = new BubbleData(context, logger, positioner, mainExecutor);
|
BubbleData data = new BubbleData(context, logger, positioner, mainExecutor);
|
||||||
return new BubbleController(context, data, synchronizer, floatingContentCoordinator,
|
return new BubbleController(context, data, synchronizer, floatingContentCoordinator,
|
||||||
new BubbleDataRepository(context, launcherApps, mainExecutor),
|
new BubbleDataRepository(context, launcherApps, mainExecutor),
|
||||||
statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
|
statusBarService, windowManager, windowManagerShellWrapper, userManager,
|
||||||
logger, taskStackListener, organizer, positioner, displayController,
|
launcherApps, logger, taskStackListener, organizer, positioner, displayController,
|
||||||
oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
|
oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
|
||||||
taskViewTransitions, syncQueue);
|
taskViewTransitions, syncQueue);
|
||||||
}
|
}
|
||||||
@@ -266,6 +269,7 @@ public class BubbleController {
|
|||||||
@Nullable IStatusBarService statusBarService,
|
@Nullable IStatusBarService statusBarService,
|
||||||
WindowManager windowManager,
|
WindowManager windowManager,
|
||||||
WindowManagerShellWrapper windowManagerShellWrapper,
|
WindowManagerShellWrapper windowManagerShellWrapper,
|
||||||
|
UserManager userManager,
|
||||||
LauncherApps launcherApps,
|
LauncherApps launcherApps,
|
||||||
BubbleLogger bubbleLogger,
|
BubbleLogger bubbleLogger,
|
||||||
TaskStackListenerImpl taskStackListener,
|
TaskStackListenerImpl taskStackListener,
|
||||||
@@ -287,6 +291,7 @@ public class BubbleController {
|
|||||||
: statusBarService;
|
: statusBarService;
|
||||||
mWindowManager = windowManager;
|
mWindowManager = windowManager;
|
||||||
mWindowManagerShellWrapper = windowManagerShellWrapper;
|
mWindowManagerShellWrapper = windowManagerShellWrapper;
|
||||||
|
mUserManager = userManager;
|
||||||
mFloatingContentCoordinator = floatingContentCoordinator;
|
mFloatingContentCoordinator = floatingContentCoordinator;
|
||||||
mDataRepository = dataRepository;
|
mDataRepository = dataRepository;
|
||||||
mLogger = bubbleLogger;
|
mLogger = bubbleLogger;
|
||||||
@@ -447,6 +452,10 @@ public class BubbleController {
|
|||||||
|
|
||||||
mOneHandedOptional.ifPresent(this::registerOneHandedState);
|
mOneHandedOptional.ifPresent(this::registerOneHandedState);
|
||||||
mDragAndDropController.addListener(this::collapseStack);
|
mDragAndDropController.addListener(this::collapseStack);
|
||||||
|
|
||||||
|
// Clear out any persisted bubbles on disk that no longer have a valid user.
|
||||||
|
List<UserInfo> users = mUserManager.getAliveUsers();
|
||||||
|
mDataRepository.sanitizeBubbles(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -590,6 +599,17 @@ public class BubbleController {
|
|||||||
mCurrentProfiles = currentProfiles;
|
mCurrentProfiles = currentProfiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Called when a user is removed from the device, including work profiles. */
|
||||||
|
public void onUserRemoved(int removedUserId) {
|
||||||
|
UserInfo parent = mUserManager.getProfileParent(removedUserId);
|
||||||
|
int parentUserId = parent != null ? parent.getUserHandle().getIdentifier() : -1;
|
||||||
|
mBubbleData.removeBubblesForUser(removedUserId);
|
||||||
|
// Typically calls from BubbleData would remove bubbles from the DataRepository as well,
|
||||||
|
// however, this gets complicated when users are removed (mCurrentUserId won't necessarily
|
||||||
|
// be correct for this) so we update the repo directly.
|
||||||
|
mDataRepository.removeBubblesForUser(removedUserId, parentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
/** Whether this userId belongs to the current user. */
|
/** Whether this userId belongs to the current user. */
|
||||||
private boolean isCurrentProfile(int userId) {
|
private boolean isCurrentProfile(int userId) {
|
||||||
return userId == UserHandle.USER_ALL
|
return userId == UserHandle.USER_ALL
|
||||||
@@ -1808,6 +1828,13 @@ public class BubbleController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUserRemoved(int removedUserId) {
|
||||||
|
mMainExecutor.execute(() -> {
|
||||||
|
BubbleController.this.onUserRemoved(removedUserId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigChanged(Configuration newConfig) {
|
public void onConfigChanged(Configuration newConfig) {
|
||||||
mMainExecutor.execute(() -> {
|
mMainExecutor.execute(() -> {
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ public class BubbleData {
|
|||||||
getOverflowBubbles(), invalidBubblesFromPackage, removeBubble);
|
getOverflowBubbles(), invalidBubblesFromPackage, removeBubble);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dismisses all bubbles from the given package. */
|
/** Removes all bubbles from the given package. */
|
||||||
public void removeBubblesWithPackageName(String packageName, int reason) {
|
public void removeBubblesWithPackageName(String packageName, int reason) {
|
||||||
final Predicate<Bubble> bubbleMatchesPackage = bubble ->
|
final Predicate<Bubble> bubbleMatchesPackage = bubble ->
|
||||||
bubble.getPackageName().equals(packageName);
|
bubble.getPackageName().equals(packageName);
|
||||||
@@ -477,6 +477,18 @@ public class BubbleData {
|
|||||||
performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble);
|
performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Removes all bubbles for the given user. */
|
||||||
|
public void removeBubblesForUser(int userId) {
|
||||||
|
List<Bubble> removedBubbles = filterAllBubbles(bubble ->
|
||||||
|
userId == bubble.getUser().getIdentifier());
|
||||||
|
for (Bubble b : removedBubbles) {
|
||||||
|
doRemove(b.getKey(), Bubbles.DISMISS_USER_REMOVED);
|
||||||
|
}
|
||||||
|
if (!removedBubbles.isEmpty()) {
|
||||||
|
dispatchPendingChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void doAdd(Bubble bubble) {
|
private void doAdd(Bubble bubble) {
|
||||||
if (DEBUG_BUBBLE_DATA) {
|
if (DEBUG_BUBBLE_DATA) {
|
||||||
Log.d(TAG, "doAdd: " + bubble);
|
Log.d(TAG, "doAdd: " + bubble);
|
||||||
@@ -552,7 +564,8 @@ public class BubbleData {
|
|||||||
|| reason == Bubbles.DISMISS_BLOCKED
|
|| reason == Bubbles.DISMISS_BLOCKED
|
||||||
|| reason == Bubbles.DISMISS_SHORTCUT_REMOVED
|
|| reason == Bubbles.DISMISS_SHORTCUT_REMOVED
|
||||||
|| reason == Bubbles.DISMISS_PACKAGE_REMOVED
|
|| reason == Bubbles.DISMISS_PACKAGE_REMOVED
|
||||||
|| reason == Bubbles.DISMISS_USER_CHANGED;
|
|| reason == Bubbles.DISMISS_USER_CHANGED
|
||||||
|
|| reason == Bubbles.DISMISS_USER_REMOVED;
|
||||||
|
|
||||||
int indexToRemove = indexForKey(key);
|
int indexToRemove = indexForKey(key);
|
||||||
if (indexToRemove == -1) {
|
if (indexToRemove == -1) {
|
||||||
@@ -1073,6 +1086,35 @@ public class BubbleData {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of bubbles that match the provided predicate. This checks all types of
|
||||||
|
* bubbles (i.e. pending, suppressed, active, and overflowed).
|
||||||
|
*/
|
||||||
|
private List<Bubble> filterAllBubbles(Predicate<Bubble> predicate) {
|
||||||
|
ArrayList<Bubble> matchingBubbles = new ArrayList<>();
|
||||||
|
for (Bubble b : mPendingBubbles.values()) {
|
||||||
|
if (predicate.test(b)) {
|
||||||
|
matchingBubbles.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Bubble b : mSuppressedBubbles.values()) {
|
||||||
|
if (predicate.test(b)) {
|
||||||
|
matchingBubbles.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Bubble b : mBubbles) {
|
||||||
|
if (predicate.test(b)) {
|
||||||
|
matchingBubbles.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Bubble b : mOverflowBubbles) {
|
||||||
|
if (predicate.test(b)) {
|
||||||
|
matchingBubbles.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchingBubbles;
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting(visibility = PRIVATE)
|
@VisibleForTesting(visibility = PRIVATE)
|
||||||
void setTimeSource(TimeSource timeSource) {
|
void setTimeSource(TimeSource timeSource) {
|
||||||
mTimeSource = timeSource;
|
mTimeSource = timeSource;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import android.content.pm.LauncherApps
|
|||||||
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
|
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
|
||||||
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC
|
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC
|
||||||
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
|
import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
|
||||||
|
import android.content.pm.UserInfo
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.wm.shell.bubbles.storage.BubbleEntity
|
import com.android.wm.shell.bubbles.storage.BubbleEntity
|
||||||
@@ -73,6 +74,22 @@ internal class BubbleDataRepository(
|
|||||||
if (entities.isNotEmpty()) persistToDisk()
|
if (entities.isNotEmpty()) persistToDisk()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all the bubbles associated with the provided user from memory. Then persists the
|
||||||
|
* snapshot to disk asynchronously.
|
||||||
|
*/
|
||||||
|
fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentId: Int) {
|
||||||
|
if (volatileRepository.removeBubblesForUser(userId, parentId)) persistToDisk()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any bubbles that don't have a user id from the provided list of users.
|
||||||
|
*/
|
||||||
|
fun sanitizeBubbles(users: List<UserInfo>) {
|
||||||
|
val userIds = users.map { u -> u.id }
|
||||||
|
if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk()
|
||||||
|
}
|
||||||
|
|
||||||
private fun transform(bubbles: List<Bubble>): List<BubbleEntity> {
|
private fun transform(bubbles: List<Bubble>): List<BubbleEntity> {
|
||||||
return bubbles.mapNotNull { b ->
|
return bubbles.mapNotNull { b ->
|
||||||
BubbleEntity(
|
BubbleEntity(
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public interface Bubbles {
|
|||||||
DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
|
DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
|
||||||
DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT,
|
DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT,
|
||||||
DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED,
|
DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED,
|
||||||
DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK})
|
DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED})
|
||||||
@Target({FIELD, LOCAL_VARIABLE, PARAMETER})
|
@Target({FIELD, LOCAL_VARIABLE, PARAMETER})
|
||||||
@interface DismissReason {}
|
@interface DismissReason {}
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ public interface Bubbles {
|
|||||||
int DISMISS_PACKAGE_REMOVED = 13;
|
int DISMISS_PACKAGE_REMOVED = 13;
|
||||||
int DISMISS_NO_BUBBLE_UP = 14;
|
int DISMISS_NO_BUBBLE_UP = 14;
|
||||||
int DISMISS_RELOAD_FROM_DISK = 15;
|
int DISMISS_RELOAD_FROM_DISK = 15;
|
||||||
|
int DISMISS_USER_REMOVED = 16;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {@code true} if there is a bubble associated with the provided key and if its
|
* @return {@code true} if there is a bubble associated with the provided key and if its
|
||||||
@@ -242,6 +243,13 @@ public interface Bubbles {
|
|||||||
*/
|
*/
|
||||||
void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles);
|
void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a user is removed.
|
||||||
|
*
|
||||||
|
* @param removedUserId the id of the removed user.
|
||||||
|
*/
|
||||||
|
void onUserRemoved(int removedUserId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when config changed.
|
* Called when config changed.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.android.wm.shell.bubbles.storage
|
package com.android.wm.shell.bubbles.storage
|
||||||
|
|
||||||
|
import android.annotation.UserIdInt
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
@@ -95,10 +96,63 @@ class BubbleVolatileRepository(private val launcherApps: LauncherApps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun removeBubbles(userId: Int, bubbles: List<BubbleEntity>) =
|
fun removeBubbles(@UserIdInt userId: Int, bubbles: List<BubbleEntity>) =
|
||||||
uncache(bubbles.filter { b: BubbleEntity ->
|
uncache(bubbles.filter { b: BubbleEntity ->
|
||||||
getEntities(userId).removeIf { e: BubbleEntity -> b.key == e.key } })
|
getEntities(userId).removeIf { e: BubbleEntity -> b.key == e.key } })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all the bubbles associated with the provided userId.
|
||||||
|
* @return whether bubbles were removed or not.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentUserId: Int): Boolean {
|
||||||
|
if (parentUserId != -1) {
|
||||||
|
return removeBubblesForUserWithParent(userId, parentUserId)
|
||||||
|
} else {
|
||||||
|
val entities = entitiesByUser.get(userId)
|
||||||
|
entitiesByUser.remove(userId)
|
||||||
|
return entities != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all the bubbles associated with the provided userId when that userId is part of
|
||||||
|
* a profile (e.g. managed account).
|
||||||
|
*
|
||||||
|
* @return whether bubbles were removed or not.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
private fun removeBubblesForUserWithParent(
|
||||||
|
@UserIdInt userId: Int,
|
||||||
|
@UserIdInt parentUserId: Int
|
||||||
|
): Boolean {
|
||||||
|
return entitiesByUser.get(parentUserId).removeIf { b: BubbleEntity -> b.userId == userId }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Goes through all the persisted bubbles and removes them if the user is not in the active
|
||||||
|
* list of users.
|
||||||
|
*
|
||||||
|
* @return whether the list of bubbles changed or not (i.e. was a removal made).
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun sanitizeBubbles(activeUsers: List<Int>): Boolean {
|
||||||
|
for (i in 0 until entitiesByUser.size()) {
|
||||||
|
// First check if the user is a parent / top-level user
|
||||||
|
val parentUserId = entitiesByUser.keyAt(i)
|
||||||
|
if (!activeUsers.contains(parentUserId)) {
|
||||||
|
return removeBubblesForUser(parentUserId, -1)
|
||||||
|
} else {
|
||||||
|
// Then check if each of the bubbles in the top-level user, still has a valid user
|
||||||
|
// as it could belong to a profile and have a different id from the parent.
|
||||||
|
return entitiesByUser.get(parentUserId).removeIf { b: BubbleEntity ->
|
||||||
|
!activeUsers.contains(b.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun cache(bubbles: List<BubbleEntity>) {
|
private fun cache(bubbles: List<BubbleEntity>) {
|
||||||
bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) ->
|
bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) ->
|
||||||
launcherApps.cacheShortcuts(key.pkg, bubbles.map { it.shortcutId },
|
launcherApps.cacheShortcuts(key.pkg, bubbles.map { it.shortcutId },
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import android.animation.AnimationHandler;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.LauncherApps;
|
import android.content.pm.LauncherApps;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.UserManager;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.android.internal.jank.InteractionJankMonitor;
|
import com.android.internal.jank.InteractionJankMonitor;
|
||||||
@@ -106,6 +107,7 @@ public class WMShellModule {
|
|||||||
IStatusBarService statusBarService,
|
IStatusBarService statusBarService,
|
||||||
WindowManager windowManager,
|
WindowManager windowManager,
|
||||||
WindowManagerShellWrapper windowManagerShellWrapper,
|
WindowManagerShellWrapper windowManagerShellWrapper,
|
||||||
|
UserManager userManager,
|
||||||
LauncherApps launcherApps,
|
LauncherApps launcherApps,
|
||||||
TaskStackListenerImpl taskStackListener,
|
TaskStackListenerImpl taskStackListener,
|
||||||
UiEventLogger uiEventLogger,
|
UiEventLogger uiEventLogger,
|
||||||
@@ -120,7 +122,7 @@ public class WMShellModule {
|
|||||||
SyncTransactionQueue syncQueue) {
|
SyncTransactionQueue syncQueue) {
|
||||||
return BubbleController.create(context, null /* synchronizer */,
|
return BubbleController.create(context, null /* synchronizer */,
|
||||||
floatingContentCoordinator, statusBarService, windowManager,
|
floatingContentCoordinator, statusBarService, windowManager,
|
||||||
windowManagerShellWrapper, launcherApps, taskStackListener,
|
windowManagerShellWrapper, userManager, launcherApps, taskStackListener,
|
||||||
uiEventLogger, organizer, displayController, oneHandedOptional,
|
uiEventLogger, organizer, displayController, oneHandedOptional,
|
||||||
dragAndDropController, mainExecutor, mainHandler, bgExecutor,
|
dragAndDropController, mainExecutor, mainHandler, bgExecutor,
|
||||||
taskViewTransitions, syncQueue);
|
taskViewTransitions, syncQueue);
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ public class BubbleDataTest extends ShellTestCase {
|
|||||||
mEntryA3 = createBubbleEntry(1, "a3", "package.a", null);
|
mEntryA3 = createBubbleEntry(1, "a3", "package.a", null);
|
||||||
mEntryB1 = createBubbleEntry(1, "b1", "package.b", null);
|
mEntryB1 = createBubbleEntry(1, "b1", "package.b", null);
|
||||||
mEntryB2 = createBubbleEntry(1, "b2", "package.b", null);
|
mEntryB2 = createBubbleEntry(1, "b2", "package.b", null);
|
||||||
mEntryB3 = createBubbleEntry(1, "b3", "package.b", null);
|
mEntryB3 = createBubbleEntry(11, "b3", "package.b", null);
|
||||||
mEntryC1 = createBubbleEntry(1, "c1", "package.c", null);
|
mEntryC1 = createBubbleEntry(11, "c1", "package.c", null);
|
||||||
|
|
||||||
NotificationListenerService.Ranking ranking =
|
NotificationListenerService.Ranking ranking =
|
||||||
mock(NotificationListenerService.Ranking.class);
|
mock(NotificationListenerService.Ranking.class);
|
||||||
@@ -1058,6 +1058,37 @@ public class BubbleDataTest extends ShellTestCase {
|
|||||||
assertBubbleListContains(mBubbleA2, mBubbleA1, mBubbleLocusId);
|
assertBubbleListContains(mBubbleA2, mBubbleA1, mBubbleLocusId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_removeBubblesForUser() {
|
||||||
|
// A is user 1
|
||||||
|
sendUpdatedEntryAtTime(mEntryA1, 2000);
|
||||||
|
sendUpdatedEntryAtTime(mEntryA2, 3000);
|
||||||
|
// B & C belong to user 11
|
||||||
|
sendUpdatedEntryAtTime(mEntryB3, 4000);
|
||||||
|
sendUpdatedEntryAtTime(mEntryC1, 5000);
|
||||||
|
mBubbleData.setListener(mListener);
|
||||||
|
|
||||||
|
mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||||
|
verifyUpdateReceived();
|
||||||
|
assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
|
||||||
|
assertBubbleListContains(mBubbleC1, mBubbleB3, mBubbleA2);
|
||||||
|
|
||||||
|
// Remove all the A bubbles
|
||||||
|
mBubbleData.removeBubblesForUser(1);
|
||||||
|
verifyUpdateReceived();
|
||||||
|
|
||||||
|
// Verify the update has the removals.
|
||||||
|
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||||
|
assertThat(update.removedBubbles.get(0)).isEqualTo(
|
||||||
|
Pair.create(mBubbleA2, Bubbles.DISMISS_USER_REMOVED));
|
||||||
|
assertThat(update.removedBubbles.get(1)).isEqualTo(
|
||||||
|
Pair.create(mBubbleA1, Bubbles.DISMISS_USER_REMOVED));
|
||||||
|
|
||||||
|
// Verify no A bubbles in active or overflow.
|
||||||
|
assertBubbleListContains(mBubbleC1, mBubbleB3);
|
||||||
|
assertOverflowChangedTo(ImmutableList.of());
|
||||||
|
}
|
||||||
|
|
||||||
private void verifyUpdateReceived() {
|
private void verifyUpdateReceived() {
|
||||||
verify(mListener).applyUpdate(mUpdateCaptor.capture());
|
verify(mListener).applyUpdate(mUpdateCaptor.capture());
|
||||||
reset(mListener);
|
reset(mListener);
|
||||||
|
|||||||
@@ -23,14 +23,19 @@ import android.testing.AndroidTestingRunner
|
|||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import com.android.wm.shell.ShellTestCase
|
import com.android.wm.shell.ShellTestCase
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
import junit.framework.Assert.assertEquals
|
import junit.framework.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.ArgumentMatchers.any
|
||||||
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
import org.mockito.ArgumentMatchers.eq
|
import org.mockito.ArgumentMatchers.eq
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.Mockito.mock
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.never
|
||||||
import org.mockito.Mockito.reset
|
import org.mockito.Mockito.reset
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@RunWith(AndroidTestingRunner::class)
|
@RunWith(AndroidTestingRunner::class)
|
||||||
@@ -41,17 +46,17 @@ class BubbleVolatileRepositoryTest : ShellTestCase() {
|
|||||||
private val user11 = UserHandle.of(11)
|
private val user11 = UserHandle.of(11)
|
||||||
|
|
||||||
// user, package, shortcut, notification key, height, res-height, title, taskId, locusId
|
// user, package, shortcut, notification key, height, res-height, title, taskId, locusId
|
||||||
private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1",
|
private val bubble1 = BubbleEntity(user0.identifier,
|
||||||
"0key-1", 120, 0, null, 1, null)
|
"com.example.messenger", "shortcut-1", "0key-1", 120, 0, null, 1, null)
|
||||||
private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob",
|
private val bubble2 = BubbleEntity(user10_managed.identifier,
|
||||||
"10key-2", 0, 16537428, "title", 2, null)
|
"com.example.chat", "alice and bob", "10key-2", 0, 16537428, "title", 2, null)
|
||||||
private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2",
|
private val bubble3 = BubbleEntity(user0.identifier,
|
||||||
"0key-3", 120, 0, null, INVALID_TASK_ID, null)
|
"com.example.messenger", "shortcut-2", "0key-3", 120, 0, null, INVALID_TASK_ID, null)
|
||||||
|
|
||||||
private val bubble11 = BubbleEntity(11, "com.example.messenger",
|
private val bubble11 = BubbleEntity(user11.identifier,
|
||||||
"shortcut-1", "01key-1", 120, 0, null, 3)
|
"com.example.messenger", "shortcut-1", "01key-1", 120, 0, null, 3)
|
||||||
private val bubble12 = BubbleEntity(11, "com.example.chat", "alice and bob",
|
private val bubble12 = BubbleEntity(user11.identifier,
|
||||||
"11key-2", 0, 16537428, "title", INVALID_TASK_ID)
|
"com.example.chat", "alice and bob", "11key-2", 0, 16537428, "title", INVALID_TASK_ID)
|
||||||
|
|
||||||
private val user0bubbles = listOf(bubble1, bubble2, bubble3)
|
private val user0bubbles = listOf(bubble1, bubble2, bubble3)
|
||||||
private val user11bubbles = listOf(bubble11, bubble12)
|
private val user11bubbles = listOf(bubble11, bubble12)
|
||||||
@@ -151,6 +156,119 @@ class BubbleVolatileRepositoryTest : ShellTestCase() {
|
|||||||
repository.addBubbles(user0.identifier, listOf(bubbleModified))
|
repository.addBubbles(user0.identifier, listOf(bubbleModified))
|
||||||
assertEquals(bubbleModified, repository.getEntities(user0.identifier).get(0))
|
assertEquals(bubbleModified, repository.getEntities(user0.identifier).get(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRemoveBubblesForUser() {
|
||||||
|
repository.addBubbles(user0.identifier, user0bubbles)
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble2, bubble3))
|
||||||
|
|
||||||
|
val ret = repository.removeBubblesForUser(user0.identifier, -1)
|
||||||
|
assertThat(ret).isTrue() // bubbles were removed
|
||||||
|
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList()).isEmpty()
|
||||||
|
verify(launcherApps, never()).uncacheShortcuts(anyString(),
|
||||||
|
any(),
|
||||||
|
any(UserHandle::class.java), anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRemoveBubblesForUser_parentUserRemoved() {
|
||||||
|
repository.addBubbles(user0.identifier, user0bubbles)
|
||||||
|
// bubble2 is the work profile bubble
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble2, bubble3))
|
||||||
|
|
||||||
|
val ret = repository.removeBubblesForUser(user10_managed.identifier, user0.identifier)
|
||||||
|
assertThat(ret).isTrue() // bubbles were removed
|
||||||
|
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble3))
|
||||||
|
verify(launcherApps, never()).uncacheShortcuts(anyString(),
|
||||||
|
any(),
|
||||||
|
any(UserHandle::class.java), anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRemoveBubblesForUser_withoutBubbles() {
|
||||||
|
repository.addBubbles(user0.identifier, user0bubbles)
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble2, bubble3))
|
||||||
|
|
||||||
|
val ret = repository.removeBubblesForUser(user11.identifier, -1)
|
||||||
|
assertThat(ret).isFalse() // bubbles were NOT removed
|
||||||
|
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble2, bubble3))
|
||||||
|
verify(launcherApps, never()).uncacheShortcuts(anyString(),
|
||||||
|
any(),
|
||||||
|
any(UserHandle::class.java), anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSanitizeBubbles_noChanges() {
|
||||||
|
repository.addBubbles(user0.identifier, user0bubbles)
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble2, bubble3))
|
||||||
|
repository.addBubbles(user11.identifier, user11bubbles)
|
||||||
|
assertThat(repository.getEntities(user11.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble11, bubble12))
|
||||||
|
|
||||||
|
val ret = repository.sanitizeBubbles(listOf(user0.identifier,
|
||||||
|
user10_managed.identifier,
|
||||||
|
user11.identifier))
|
||||||
|
assertThat(ret).isFalse() // bubbles were NOT removed
|
||||||
|
|
||||||
|
verify(launcherApps, never()).uncacheShortcuts(anyString(),
|
||||||
|
any(),
|
||||||
|
any(UserHandle::class.java), anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSanitizeBubbles_userRemoved() {
|
||||||
|
repository.addBubbles(user0.identifier, user0bubbles)
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble2, bubble3))
|
||||||
|
repository.addBubbles(user11.identifier, user11bubbles)
|
||||||
|
assertThat(repository.getEntities(user11.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble11, bubble12))
|
||||||
|
|
||||||
|
val ret = repository.sanitizeBubbles(listOf(user11.identifier))
|
||||||
|
assertThat(ret).isTrue() // bubbles were removed
|
||||||
|
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList()).isEmpty()
|
||||||
|
verify(launcherApps, never()).uncacheShortcuts(anyString(),
|
||||||
|
any(),
|
||||||
|
any(UserHandle::class.java), anyInt())
|
||||||
|
|
||||||
|
// User 11 bubbles should still be here
|
||||||
|
assertThat(repository.getEntities(user11.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble11, bubble12))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSanitizeBubbles_userParentRemoved() {
|
||||||
|
repository.addBubbles(user0.identifier, user0bubbles)
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble2, bubble3))
|
||||||
|
|
||||||
|
repository.addBubbles(user11.identifier, user11bubbles)
|
||||||
|
assertThat(repository.getEntities(user11.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble11, bubble12))
|
||||||
|
|
||||||
|
val ret = repository.sanitizeBubbles(listOf(user0.identifier, user11.identifier))
|
||||||
|
assertThat(ret).isTrue() // bubbles were removed
|
||||||
|
// bubble2 is the work profile bubble and should be removed
|
||||||
|
assertThat(repository.getEntities(user0.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble1, bubble3))
|
||||||
|
verify(launcherApps, never()).uncacheShortcuts(anyString(),
|
||||||
|
any(),
|
||||||
|
any(UserHandle::class.java), anyInt())
|
||||||
|
|
||||||
|
// User 11 bubbles should still be here
|
||||||
|
assertThat(repository.getEntities(user11.identifier).toList())
|
||||||
|
.isEqualTo(listOf(bubble11, bubble12))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val PKG_MESSENGER = "com.example.messenger"
|
private const val PKG_MESSENGER = "com.example.messenger"
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ public interface NotificationLockscreenUserManager {
|
|||||||
interface UserChangedListener {
|
interface UserChangedListener {
|
||||||
default void onUserChanged(int userId) {}
|
default void onUserChanged(int userId) {}
|
||||||
default void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {}
|
default void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {}
|
||||||
|
default void onUserRemoved(int userId) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Used to hide notifications on the lockscreen */
|
/** Used to hide notifications on the lockscreen */
|
||||||
|
|||||||
@@ -152,6 +152,15 @@ public class NotificationLockscreenUserManagerImpl implements
|
|||||||
listener.onUserChanged(mCurrentUserId);
|
listener.onUserChanged(mCurrentUserId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Intent.ACTION_USER_REMOVED:
|
||||||
|
int removedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
|
||||||
|
if (removedUserId != -1) {
|
||||||
|
for (UserChangedListener listener : mListeners) {
|
||||||
|
listener.onUserRemoved(removedUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateCurrentProfilesCache();
|
||||||
|
break;
|
||||||
case Intent.ACTION_USER_ADDED:
|
case Intent.ACTION_USER_ADDED:
|
||||||
case Intent.ACTION_MANAGED_PROFILE_AVAILABLE:
|
case Intent.ACTION_MANAGED_PROFILE_AVAILABLE:
|
||||||
case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE:
|
case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE:
|
||||||
@@ -303,6 +312,7 @@ public class NotificationLockscreenUserManagerImpl implements
|
|||||||
IntentFilter filter = new IntentFilter();
|
IntentFilter filter = new IntentFilter();
|
||||||
filter.addAction(Intent.ACTION_USER_SWITCHED);
|
filter.addAction(Intent.ACTION_USER_SWITCHED);
|
||||||
filter.addAction(Intent.ACTION_USER_ADDED);
|
filter.addAction(Intent.ACTION_USER_ADDED);
|
||||||
|
filter.addAction(Intent.ACTION_USER_REMOVED);
|
||||||
filter.addAction(Intent.ACTION_USER_UNLOCKED);
|
filter.addAction(Intent.ACTION_USER_UNLOCKED);
|
||||||
filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
|
filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
|
||||||
filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
|
filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
|
||||||
|
|||||||
@@ -271,6 +271,11 @@ public class BubblesManager implements Dumpable {
|
|||||||
mBubbles.onCurrentProfilesChanged(currentProfiles);
|
mBubbles.onCurrentProfilesChanged(currentProfiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUserRemoved(int userId) {
|
||||||
|
mBubbles.onUserRemoved(userId);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mSysuiProxy = new Bubbles.SysuiProxy() {
|
mSysuiProxy = new Bubbles.SysuiProxy() {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import android.hardware.face.FaceManager;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
|
import android.os.UserManager;
|
||||||
import android.service.dreams.IDreamManager;
|
import android.service.dreams.IDreamManager;
|
||||||
import android.service.notification.NotificationListenerService;
|
import android.service.notification.NotificationListenerService;
|
||||||
import android.service.notification.ZenModeConfig;
|
import android.service.notification.ZenModeConfig;
|
||||||
@@ -365,6 +366,7 @@ public class BubblesTest extends SysuiTestCase {
|
|||||||
mStatusBarService,
|
mStatusBarService,
|
||||||
mWindowManager,
|
mWindowManager,
|
||||||
mWindowManagerShellWrapper,
|
mWindowManagerShellWrapper,
|
||||||
|
mock(UserManager.class),
|
||||||
mLauncherApps,
|
mLauncherApps,
|
||||||
mBubbleLogger,
|
mBubbleLogger,
|
||||||
mTaskStackListener,
|
mTaskStackListener,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import android.hardware.display.AmbientDisplayConfiguration;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
|
import android.os.UserManager;
|
||||||
import android.service.dreams.IDreamManager;
|
import android.service.dreams.IDreamManager;
|
||||||
import android.service.notification.NotificationListenerService;
|
import android.service.notification.NotificationListenerService;
|
||||||
import android.service.notification.ZenModeConfig;
|
import android.service.notification.ZenModeConfig;
|
||||||
@@ -330,6 +331,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
|
|||||||
mStatusBarService,
|
mStatusBarService,
|
||||||
mWindowManager,
|
mWindowManager,
|
||||||
mWindowManagerShellWrapper,
|
mWindowManagerShellWrapper,
|
||||||
|
mock(UserManager.class),
|
||||||
mLauncherApps,
|
mLauncherApps,
|
||||||
mBubbleLogger,
|
mBubbleLogger,
|
||||||
mTaskStackListener,
|
mTaskStackListener,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package com.android.systemui.wmshell;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.LauncherApps;
|
import android.content.pm.LauncherApps;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.UserManager;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.android.internal.statusbar.IStatusBarService;
|
import com.android.internal.statusbar.IStatusBarService;
|
||||||
@@ -53,6 +54,7 @@ public class TestableBubbleController extends BubbleController {
|
|||||||
IStatusBarService statusBarService,
|
IStatusBarService statusBarService,
|
||||||
WindowManager windowManager,
|
WindowManager windowManager,
|
||||||
WindowManagerShellWrapper windowManagerShellWrapper,
|
WindowManagerShellWrapper windowManagerShellWrapper,
|
||||||
|
UserManager userManager,
|
||||||
LauncherApps launcherApps,
|
LauncherApps launcherApps,
|
||||||
BubbleLogger bubbleLogger,
|
BubbleLogger bubbleLogger,
|
||||||
TaskStackListenerImpl taskStackListener,
|
TaskStackListenerImpl taskStackListener,
|
||||||
@@ -66,10 +68,10 @@ public class TestableBubbleController extends BubbleController {
|
|||||||
TaskViewTransitions taskViewTransitions,
|
TaskViewTransitions taskViewTransitions,
|
||||||
SyncTransactionQueue syncQueue) {
|
SyncTransactionQueue syncQueue) {
|
||||||
super(context, data, Runnable::run, floatingContentCoordinator, dataRepository,
|
super(context, data, Runnable::run, floatingContentCoordinator, dataRepository,
|
||||||
statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
|
statusBarService, windowManager, windowManagerShellWrapper, userManager,
|
||||||
bubbleLogger, taskStackListener, shellTaskOrganizer, positioner, displayController,
|
launcherApps, bubbleLogger, taskStackListener, shellTaskOrganizer, positioner,
|
||||||
oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
|
displayController, oneHandedOptional, dragAndDropController, shellMainExecutor,
|
||||||
new SyncExecutor(), taskViewTransitions, syncQueue);
|
shellMainHandler, new SyncExecutor(), taskViewTransitions, syncQueue);
|
||||||
setInflateSynchronously(true);
|
setInflateSynchronously(true);
|
||||||
initialize();
|
initialize();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user