diff options
28 files changed, 826 insertions, 88 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 9f4a4e08bbfd..689ccf7ffc36 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -55,6 +55,7 @@ import com.android.systemui.shared.system.DevicePolicyManagerWrapper; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.statusbar.AmbientPulseManager; import com.android.systemui.statusbar.NavigationBarController; +import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; @@ -302,6 +303,7 @@ public class Dependency extends SystemUI { @Inject Lazy<ChannelEditorDialogController> mChannelEditorDialogController; @Inject Lazy<INotificationManager> mINotificationManager; @Inject Lazy<FalsingManager> mFalsingManager; + @Inject Lazy<NotificationClickNotifier> mClickNotifier; @Inject public Dependency() { @@ -479,6 +481,7 @@ public class Dependency extends SystemUI { mProviders.put(ChannelEditorDialogController.class, mChannelEditorDialogController::get); mProviders.put(INotificationManager.class, mINotificationManager::get); mProviders.put(FalsingManager.class, mFalsingManager::get); + mProviders.put(NotificationClickNotifier.class, mClickNotifier::get); // TODO(b/118592525): to support multi-display , we start to add something which is // per-display, while others may be global. I think it's time to add diff --git a/packages/SystemUI/src/com/android/systemui/DependencyBinder.java b/packages/SystemUI/src/com/android/systemui/DependencyBinder.java index 057d70ccdc0d..79d8b17e6d6a 100644 --- a/packages/SystemUI/src/com/android/systemui/DependencyBinder.java +++ b/packages/SystemUI/src/com/android/systemui/DependencyBinder.java @@ -70,6 +70,8 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.ZenModeControllerImpl; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerServiceImpl; +import com.android.systemui.util.time.SystemClock; +import com.android.systemui.util.time.SystemClockImpl; import com.android.systemui.volume.VolumeDialogControllerImpl; import dagger.Binds; @@ -241,4 +243,9 @@ public abstract class DependencyBinder { */ @Binds public abstract FalsingManager provideFalsingmanager(FalsingManagerProxy falsingManagerImpl); + + /** + */ + @Binds + public abstract SystemClock provideSystemClock(SystemClockImpl systemClock); } diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender.java index 05acdd080aa5..7db6642276f8 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender.java @@ -23,8 +23,12 @@ import android.os.Looper; import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.statusbar.NotificationInteractionTracker; import com.android.systemui.statusbar.NotificationLifetimeExtender; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.util.time.SystemClock; + +import javax.inject.Inject; /** * Extends the lifetime of foreground notification services such that they show for at least @@ -39,8 +43,15 @@ public class ForegroundServiceLifetimeExtender implements NotificationLifetimeEx private NotificationSafeToRemoveCallback mNotificationSafeToRemoveCallback; private ArraySet<NotificationEntry> mManagedEntries = new ArraySet<>(); private Handler mHandler = new Handler(Looper.getMainLooper()); + private final SystemClock mSystemClock; + private final NotificationInteractionTracker mInteractionTracker; - public ForegroundServiceLifetimeExtender() { + @Inject + public ForegroundServiceLifetimeExtender( + NotificationInteractionTracker interactionTracker, + SystemClock systemClock) { + mSystemClock = systemClock; + mInteractionTracker = interactionTracker; } @Override @@ -55,8 +66,9 @@ public class ForegroundServiceLifetimeExtender implements NotificationLifetimeEx return false; } - long currentTime = System.currentTimeMillis(); - return currentTime - entry.notification.getPostTime() < MIN_FGS_TIME_MS; + boolean hasInteracted = mInteractionTracker.hasUserInteractedWith(entry.key); + long aliveTime = mSystemClock.uptimeMillis() - entry.getCreationTime(); + return aliveTime < MIN_FGS_TIME_MS && !hasInteracted; } @Override @@ -84,7 +96,7 @@ public class ForegroundServiceLifetimeExtender implements NotificationLifetimeEx } }; long delayAmt = MIN_FGS_TIME_MS - - (System.currentTimeMillis() - entry.notification.getPostTime()); + - (mSystemClock.uptimeMillis() - entry.getCreationTime()); mHandler.postDelayed(r, delayAmt); } } diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java index 0162deb55143..1e1eaf3e2cb9 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java @@ -44,7 +44,8 @@ public class ForegroundServiceNotificationListener { @Inject public ForegroundServiceNotificationListener(Context context, ForegroundServiceController foregroundServiceController, - NotificationEntryManager notificationEntryManager) { + NotificationEntryManager notificationEntryManager, + ForegroundServiceLifetimeExtender fgsLifetimeExtender) { mContext = context; mForegroundServiceController = foregroundServiceController; notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { @@ -66,9 +67,7 @@ public class ForegroundServiceNotificationListener { removeNotification(entry.notification); } }); - - notificationEntryManager.addNotificationLifetimeExtender( - new ForegroundServiceLifetimeExtender()); + notificationEntryManager.addNotificationLifetimeExtender(fgsLifetimeExtender); } /** diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index d815d95f23d5..9900a93971f8 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -41,6 +41,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.power.EnhancedEstimatesImpl; import com.android.systemui.statusbar.KeyguardIndicationController; +import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl; @@ -167,8 +168,9 @@ public class SystemUIFactory { @Singleton @Provides public NotificationLockscreenUserManager provideNotificationLockscreenUserManager( - Context context) { - return new NotificationLockscreenUserManagerImpl(context); + Context context, + NotificationClickNotifier clickNotifier) { + return new NotificationLockscreenUserManagerImpl(context, clickNotifier); } @Singleton diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt new file mode 100644 index 000000000000..0d3948853cda --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt @@ -0,0 +1,90 @@ +package com.android.systemui.statusbar + +import android.app.Notification +import android.os.Handler +import android.os.RemoteException + +import com.android.internal.statusbar.IStatusBarService +import com.android.internal.statusbar.NotificationVisibility +import com.android.systemui.Dependency +import com.android.systemui.util.Assert + +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +/** + * Class to shim calls to IStatusBarManager#onNotificationClick/#onNotificationActionClick that + * allow an in-process notification to go out (e.g., for tracking interactions) as well as + * sending the messages along to system server. + * + * NOTE: this class eats exceptions from system server, as no current client of these APIs cares + * about errors + */ +@Singleton +public class NotificationClickNotifier @Inject constructor( + val barService: IStatusBarService, + @Named(Dependency.MAIN_HANDLER_NAME) val mainHandler: Handler +) { + val listeners = mutableListOf<NotificationInteractionListener>() + + fun addNotificationInteractionListener(listener: NotificationInteractionListener) { + Assert.isMainThread() + listeners.add(listener) + } + + fun removeNotificationInteractionListener(listener: NotificationInteractionListener) { + Assert.isMainThread() + listeners.remove(listener) + } + + private fun notifyListenersAboutInteraction(key: String) { + for (l in listeners) { + l.onNotificationInteraction(key) + } + } + + fun onNotificationActionClick( + key: String, + actionIndex: Int, + action: Notification.Action, + visibility: NotificationVisibility, + generatedByAssistant: Boolean + ) { + try { + barService.onNotificationActionClick( + key, actionIndex, action, visibility, generatedByAssistant) + } catch (e: RemoteException) { + // nothing + } + + mainHandler.post { + notifyListenersAboutInteraction(key) + } + } + + fun onNotificationClick( + key: String, + visibility: NotificationVisibility + ) { + try { + barService.onNotificationClick(key, visibility) + } catch (e: RemoteException) { + // nothing + } + + mainHandler.post { + notifyListenersAboutInteraction(key) + } + } +} + +/** + * Interface for listeners to get notified when a notification is interacted with via a click or + * interaction with remote input or actions + */ +interface NotificationInteractionListener { + fun onNotificationInteraction(key: String) +} + +private const val TAG = "NotificationClickNotifier" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt new file mode 100644 index 000000000000..40a3ed64f2c2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt @@ -0,0 +1,45 @@ +package com.android.systemui.statusbar + +import com.android.internal.statusbar.NotificationVisibility +import com.android.systemui.statusbar.notification.NotificationEntryManager +import com.android.systemui.statusbar.notification.NotificationEntryListener +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Class to track user interaction with notifications. It's a glorified map of key : bool that can + * merge multiple "user interacted with notification" signals into a single place. + */ +@Singleton +class NotificationInteractionTracker @Inject constructor( + private val clicker: NotificationClickNotifier, + private val entryManager: NotificationEntryManager +) : NotificationEntryListener, NotificationInteractionListener { + private val interactions = mutableMapOf<String, Boolean>() + + init { + clicker.addNotificationInteractionListener(this) + entryManager.addNotificationEntryListener(this) + } + + fun hasUserInteractedWith(key: String): Boolean = key in interactions + + override fun onNotificationAdded(entry: NotificationEntry) { + interactions[entry.key] = false + } + + override fun onEntryRemoved( + entry: NotificationEntry, + visibility: NotificationVisibility?, + removedByUser: Boolean + ) { + interactions.remove(entry.key) + } + + override fun onNotificationInteraction(key: String) { + interactions[key] = true + } +} + +private const val TAG = "NotificationInteractionTracker" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index e08a5ae07bd8..67218fc30976 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -28,8 +28,6 @@ import android.content.IntentFilter; import android.content.IntentSender; import android.content.pm.UserInfo; import android.database.ContentObserver; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -37,7 +35,6 @@ import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; -import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; @@ -80,8 +77,8 @@ public class NotificationLockscreenUserManagerImpl implements private final SparseBooleanArray mUsersAllowingPrivateNotifications = new SparseBooleanArray(); private final SparseBooleanArray mUsersAllowingNotifications = new SparseBooleanArray(); private final UserManager mUserManager; - private final IStatusBarService mBarService; private final List<UserChangedListener> mListeners = new ArrayList<>(); + private final NotificationClickNotifier mClickNotifier; private boolean mShowLockscreenNotifications; private boolean mAllowLockscreenRemoteInput; @@ -146,11 +143,7 @@ public class NotificationLockscreenUserManagerImpl implements getEntryManager().getNotificationData().get(notificationKey)); final NotificationVisibility nv = NotificationVisibility.obtain(notificationKey, rank, count, true, location); - try { - mBarService.onNotificationClick(notificationKey, nv); - } catch (RemoteException e) { - /* ignore */ - } + mClickNotifier.onNotificationClick(notificationKey, nv); } } } @@ -171,15 +164,16 @@ public class NotificationLockscreenUserManagerImpl implements return mEntryManager; } - public NotificationLockscreenUserManagerImpl(Context context) { + public NotificationLockscreenUserManagerImpl( + Context context, + NotificationClickNotifier clickNotifier) { mContext = context; mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService( Context.DEVICE_POLICY_SERVICE); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mCurrentUserId = ActivityManager.getCurrentUser(); - mBarService = IStatusBarService.Stub.asInterface( - ServiceManager.getService(Context.STATUS_BAR_SERVICE)); Dependency.get(StatusBarStateController.class).addCallback(this); + mClickNotifier = clickNotifier; mLockPatternUtils = new LockPatternUtils(context); mKeyguardManager = context.getSystemService(KeyguardManager.class); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 1440803f1524..3898ef754ed3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -121,6 +121,7 @@ public class NotificationRemoteInputManager implements Dumpable { protected final Context mContext; private final UserManager mUserManager; private final KeyguardManager mKeyguardManager; + private final NotificationClickNotifier mClickNotifier; protected RemoteInputController mRemoteInputController; protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback @@ -203,11 +204,7 @@ public class NotificationRemoteInputManager implements Dumpable { mEntryManager.getNotificationData().get(key)); final NotificationVisibility nv = NotificationVisibility.obtain(key, rank, count, true, location); - try { - mBarService.onNotificationActionClick(key, buttonIndex, action, nv, false); - } catch (RemoteException e) { - // Ignore - } + mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false); } private StatusBarNotification getNotificationForParent(ViewParent parent) { @@ -259,7 +256,8 @@ public class NotificationRemoteInputManager implements Dumpable { SmartReplyController smartReplyController, NotificationEntryManager notificationEntryManager, Lazy<ShadeController> shadeController, - @Named(MAIN_HANDLER_NAME) Handler mainHandler) { + @Named(MAIN_HANDLER_NAME) Handler mainHandler, + NotificationClickNotifier clickNotifier) { mContext = context; mLockscreenUserManager = lockscreenUserManager; mSmartReplyController = smartReplyController; @@ -271,6 +269,7 @@ public class NotificationRemoteInputManager implements Dumpable { mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); addLifetimeExtenders(); mKeyguardManager = context.getSystemService(KeyguardManager.class); + mClickNotifier = clickNotifier; notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java index 736b9ebea5c3..2a1f864dfd67 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java @@ -38,14 +38,17 @@ import javax.inject.Singleton; public class SmartReplyController { private final IStatusBarService mBarService; private final NotificationEntryManager mEntryManager; + private final NotificationClickNotifier mClickNotifier; private Set<String> mSendingKeys = new ArraySet<>(); private Callback mCallback; @Inject public SmartReplyController(NotificationEntryManager entryManager, - IStatusBarService statusBarService) { + IStatusBarService statusBarService, + NotificationClickNotifier clickNotifier) { mBarService = statusBarService; mEntryManager = entryManager; + mClickNotifier = clickNotifier; } public void setCallback(Callback callback) { @@ -79,12 +82,8 @@ public class SmartReplyController { NotificationLogger.getNotificationLocation(entry); final NotificationVisibility nv = NotificationVisibility.obtain( entry.key, rank, count, true, location); - try { - mBarService.onNotificationActionClick( - entry.key, actionIndex, action, nv, generatedByAssistant); - } catch (RemoteException e) { - // Nothing to do, system going down - } + mClickNotifier.onNotificationActionClick( + entry.key, actionIndex, action, nv, generatedByAssistant); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java index 1aa6bc9ae5f9..848f1a0d8fdd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java @@ -20,6 +20,8 @@ import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; +import androidx.annotation.NonNull; + import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; @@ -96,7 +98,7 @@ public interface NotificationEntryListener { * @param removedByUser true if the notification was removed by a user action */ default void onEntryRemoved( - NotificationEntry entry, + @NonNull NotificationEntry entry, @Nullable NotificationVisibility visibility, boolean removedByUser) { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java index cfc1a5f2ef3d..e50c02b66604 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -21,6 +21,7 @@ import static android.service.notification.NotificationListenerService.REASON_ER import android.annotation.Nullable; import android.app.Notification; import android.content.Context; +import android.os.SystemClock; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; @@ -385,7 +386,10 @@ public class NotificationEntryManager implements NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking(); rankingMap.getRanking(key, ranking); - NotificationEntry entry = new NotificationEntry(notification, ranking); + NotificationEntry entry = new NotificationEntry( + notification, + ranking, + SystemClock.uptimeMillis()); Dependency.get(LeakDetector.class).trackInstance(entry); // Construct the expanded view. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index d157f06c03e9..10e37591eebe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -28,6 +28,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; +import android.annotation.CurrentTimeMillisLong; import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationChannel; @@ -96,6 +97,7 @@ public final class NotificationEntry { public StatusBarIconView icon; public StatusBarIconView expandedIcon; public StatusBarIconView centeredIcon; + private long mCreationTime; private boolean interruption; public boolean autoRedacted; // whether the redacted notification was generated by us public int targetSdk; @@ -148,6 +150,46 @@ public final class NotificationEntry { */ private boolean hasSentReply; + + /** + * @param sbn the StatusBarNotification from system server + * @param creationTime SystemClock.uptimeMillis of when we were created + */ + public NotificationEntry( + @NonNull StatusBarNotification sbn, + long creationTime) { + this(sbn, null, creationTime); + } + + public NotificationEntry( + @NonNull StatusBarNotification sbn, + @Nullable NotificationListenerService.Ranking ranking, + long creationTime + ) { + + mCreationTime = creationTime; + this.key = sbn.getKey(); + this.notification = sbn; + + if (ranking != null) { + populateFromRanking(ranking); + } + } + + /** + * This method exists _only_ for tests that don't know how to pass in a creation time, and + * before a NotificationEntry builder was introduced for testing. + * + * It will always set SystemClock.uptimeMillis() as the creation time + * + * @param sbn the StatusBarNotification from system server + * + * @VisibleForTesting + */ + public NotificationEntry(@NonNull StatusBarNotification sbn) { + this(sbn, null, SystemClock.uptimeMillis()); + } + /** * Whether this notification has been approved globally, at the app level, and at the channel * level for bubbling. @@ -168,6 +210,21 @@ public final class NotificationEntry { private boolean mUserDismissedBubble; /** + * A timestamp of SystemClock.uptimeMillis() of when this entry was first created, regardless + * of any changes to the data presented. It is set once on creation and will never change, and + * allows us to know exactly how long this notification has been alive for in our listener + * service. It is entirely unrelated to the information inside of the notification. + * + * This is different to Notification#when because it persists throughout updates, whereas + * system server treats every single call to notify() as a new notification and we handle + * updates to NotificationEntry locally. + */ + @CurrentTimeMillisLong + public long getCreationTime() { + return mCreationTime; + } + + /** * Whether this notification is shown to the user as a high priority notification: visible on * the lock screen/status bar and in the top section in the shade. */ @@ -175,20 +232,6 @@ public final class NotificationEntry { private boolean mIsTopBucket; - public NotificationEntry(StatusBarNotification n) { - this(n, null); - } - - public NotificationEntry( - StatusBarNotification n, - @Nullable NotificationListenerService.Ranking ranking) { - this.key = n.getKey(); - this.notification = n; - if (ranking != null) { - populateFromRanking(ranking); - } - } - public void populateFromRanking(@NonNull NotificationListenerService.Ranking ranking) { channel = ranking.getChannel(); lastAudiblyAlertedMs = ranking.getLastAudiblyAlertedMillis(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 2c305dff3246..9d0bb707832d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -181,6 +181,7 @@ import com.android.systemui.statusbar.GestureRecorder; import com.android.systemui.statusbar.KeyboardShortcuts; import com.android.systemui.statusbar.KeyguardIndicationController; import com.android.systemui.statusbar.NavigationBarController; +import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; @@ -595,6 +596,7 @@ public class StatusBar extends SystemUI implements DemoMode, }; private ActivityIntentHelper mActivityIntentHelper; private ShadeController mShadeController; + private NotificationClickNotifier mClickNotifier; @Override public void onActiveStateChanged(int code, int uid, String packageName, boolean active) { @@ -612,6 +614,7 @@ public class StatusBar extends SystemUI implements DemoMode, @Override public void start() { + mClickNotifier = Dependency.get(NotificationClickNotifier.class); mGroupManager = Dependency.get(NotificationGroupManager.class); mGroupAlertTransferHelper = Dependency.get(NotificationGroupAlertTransferHelper.class); mVisualStabilityManager = Dependency.get(VisualStabilityManager.class); @@ -1073,7 +1076,7 @@ public class StatusBar extends SystemUI implements DemoMode, mNotificationActivityStarter = new StatusBarNotificationActivityStarter(mContext, mCommandQueue, mAssistManager, mNotificationPanel, mPresenter, mEntryManager, mHeadsUpManager, activityStarter, mActivityLaunchAnimator, - mBarService, mStatusBarStateController, mKeyguardManager, mDreamManager, + mClickNotifier, mStatusBarStateController, mKeyguardManager, mDreamManager, mRemoteInputManager, mStatusBarRemoteInputCallback, mGroupManager, mLockscreenUserManager, mShadeController, mKeyguardMonitor, mNotificationInterruptionStateProvider, mMetricsLogger, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index e00d439dc1c7..8609c4fc6070 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -42,7 +42,6 @@ import android.util.Log; import android.view.RemoteAnimationAdapter; import com.android.internal.logging.MetricsLogger; -import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.ActivityIntentHelper; @@ -54,6 +53,7 @@ import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -97,7 +97,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final HeadsUpManagerPhone mHeadsUpManager; private final KeyguardManager mKeyguardManager; private final ActivityLaunchAnimator mActivityLaunchAnimator; - private final IStatusBarService mBarService; + private final NotificationClickNotifier mClickNotifier; private final CommandQueue mCommandQueue; private final IDreamManager mDreamManager; private final Handler mMainThreadHandler; @@ -116,7 +116,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit HeadsUpManagerPhone headsUpManager, ActivityStarter activityStarter, ActivityLaunchAnimator activityLaunchAnimator, - IStatusBarService statusBarService, + NotificationClickNotifier clickNotifier, StatusBarStateController statusBarStateController, KeyguardManager keyguardManager, IDreamManager dreamManager, @@ -138,7 +138,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mPresenter = presenter; mHeadsUpManager = headsUpManager; mActivityLaunchAnimator = activityLaunchAnimator; - mBarService = statusBarService; + mClickNotifier = clickNotifier; mCommandQueue = commandQueue; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; @@ -334,11 +334,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mEntryManager.getNotificationData().get(notificationKey)); final NotificationVisibility nv = NotificationVisibility.obtain(notificationKey, rank, count, true, location); - try { - mBarService.onNotificationClick(notificationKey, nv); - } catch (RemoteException ex) { - // system process is dead if we're here. - } + mClickNotifier.onNotificationClick(notificationKey, nv); + if (!isBubble) { if (parentToCancelFinal != null) { removeNotification(parentToCancelFinal); diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/DelayableExecutor.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/DelayableExecutor.java new file mode 100644 index 000000000000..c594621552e3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/DelayableExecutor.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * A sub-class of {@link Executor} that allows Runnables to be delayed and/or cancelled. + */ +public interface DelayableExecutor extends Executor { + /** + * Execute supplied Runnable on the Executors thread after a specified delay. + * + * See {@link android.os.Handler#postDelayed(Runnable, long)}. + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + default Runnable executeDelayed(Runnable r, long delayMillis) { + return executeDelayed(r, delayMillis, TimeUnit.MILLISECONDS); + } + + /** + * Execute supplied Runnable on the Executors thread after a specified delay. + * + * See {@link android.os.Handler#postDelayed(Runnable, long)}. + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue.. + */ + Runnable executeDelayed(Runnable r, long delay, TimeUnit unit); + + /** + * Execute supplied Runnable on the Executors thread at a specified uptime. + * + * See {@link android.os.Handler#postAtTime(Runnable, long)}. + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + default Runnable executeAtTime(Runnable r, long uptime) { + return executeAtTime(r, uptime, TimeUnit.MILLISECONDS); + } + + /** + * Execute supplied Runnable on the Executors thread at a specified uptime. + * + * See {@link android.os.Handler#postAtTime(Runnable, long)}. + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + Runnable executeAtTime(Runnable r, long uptimeMillis, TimeUnit unit); +} + diff --git a/packages/SystemUI/src/com/android/systemui/util/time/SystemClock.java b/packages/SystemUI/src/com/android/systemui/util/time/SystemClock.java new file mode 100644 index 000000000000..26c77b00cae8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/time/SystemClock.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.time; + +/** + * Testable wrapper around {@link android.os.SystemClock}. + * + * Dagger can inject this wrapper into your classes. The implementation just proxies calls to the + * real SystemClock. + * + * In tests, pass an instance of FakeSystemClock, which allows you to control the values returned by + * the various getters below. + */ +public interface SystemClock { + /** @see android.os.SystemClock#uptimeMillis() */ + long uptimeMillis(); + + /** @see android.os.SystemClock#elapsedRealtime() */ + long elapsedRealtime(); + + /** @see android.os.SystemClock#elapsedRealtimeNanos() */ + long elapsedRealtimeNanos(); + + /** @see android.os.SystemClock#currentThreadTimeMillis() */ + long currentThreadTimeMillis(); + + /** @see System#currentTimeMillis() */ + long currentTimeMillis(); +} + diff --git a/packages/SystemUI/src/com/android/systemui/util/time/SystemClockImpl.java b/packages/SystemUI/src/com/android/systemui/util/time/SystemClockImpl.java new file mode 100644 index 000000000000..501da990d0ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/time/SystemClockImpl.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.time; + +import javax.inject.Inject; + +/** Default implementation of {@link SystemClock}. */ +public class SystemClockImpl implements SystemClock { + @Inject + public SystemClockImpl() {} + + @Override + public long uptimeMillis() { + return android.os.SystemClock.uptimeMillis(); + } + + @Override + public long elapsedRealtime() { + return android.os.SystemClock.elapsedRealtime(); + } + + @Override + public long elapsedRealtimeNanos() { + return android.os.SystemClock.elapsedRealtimeNanos(); + } + + @Override + public long currentThreadTimeMillis() { + return android.os.SystemClock.currentThreadTimeMillis(); + } + + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } +} + diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java index 8a6ee12d7068..c1f94a5cefd3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -61,7 +62,8 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { mFsc = new ForegroundServiceController(); NotificationEntryManager notificationEntryManager = mock(NotificationEntryManager.class); mListener = new ForegroundServiceNotificationListener( - mContext, mFsc, notificationEntryManager); + mContext, mFsc, notificationEntryManager, + mock(ForegroundServiceLifetimeExtender.class)); ArgumentCaptor<NotificationEntryListener> entryListenerCaptor = ArgumentCaptor.forClass(NotificationEntryListener.class); verify(notificationEntryManager).addNotificationEntryListener( diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceLifetimeExtenderTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceNotificationListenerTest.java index b1dabdda2241..6dbe1b34c112 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceLifetimeExtenderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceNotificationListenerTest.java @@ -20,63 +20,84 @@ import static com.android.systemui.ForegroundServiceLifetimeExtender.MIN_FGS_TIM import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.app.Notification; +import android.os.UserHandle; import android.service.notification.StatusBarNotification; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.systemui.statusbar.NotificationInteractionTracker; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) @SmallTest -public class ForegroundServiceLifetimeExtenderTest extends SysuiTestCase { - private ForegroundServiceLifetimeExtender mExtender = new ForegroundServiceLifetimeExtender(); - private StatusBarNotification mSbn; +public class ForegroundServiceNotificationListenerTest extends SysuiTestCase { + private static final String TEST_PACKAGE_NAME = "test"; + private static final int TEST_UID = 0; + + private ForegroundServiceLifetimeExtender mExtender; private NotificationEntry mEntry; + private StatusBarNotification mSbn; private Notification mNotif; + private final FakeSystemClock mClock = new FakeSystemClock(); + + @Mock + private NotificationInteractionTracker mInteractionTracker; @Before public void setup() { + MockitoAnnotations.initMocks(this); + mExtender = new ForegroundServiceLifetimeExtender(mInteractionTracker, mClock); + mNotif = new Notification.Builder(mContext, "") .setSmallIcon(R.drawable.ic_person) .setContentTitle("Title") .setContentText("Text") .build(); - mSbn = mock(StatusBarNotification.class); - when(mSbn.getNotification()).thenReturn(mNotif); - - mEntry = new NotificationEntry(mSbn); + mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, + 0, mNotif, new UserHandle(ActivityManager.getCurrentUser()), null, 0); + mEntry = new NotificationEntry(mSbn, mClock.uptimeMillis()); } + /** + * ForegroundServiceLifetimeExtenderTest + */ @Test public void testShouldExtendLifetime_should_foreground() { // Extend the lifetime of a FGS notification iff it has not been visible // for the minimum time mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE; - when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis()); + + // No time has elapsed, keep showing assertTrue(mExtender.shouldExtendLifetime(mEntry)); } @Test public void testShouldExtendLifetime_shouldNot_foreground() { mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE; - when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1); + + // Entry was created at mClock.uptimeMillis(), advance it MIN_FGS_TIME_MS + 1 + mClock.advanceTime(MIN_FGS_TIME_MS + 1); assertFalse(mExtender.shouldExtendLifetime(mEntry)); } @Test public void testShouldExtendLifetime_shouldNot_notForeground() { mNotif.flags = 0; - when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1); + + // Entry was created at mClock.uptimeMillis(), advance it MIN_FGS_TIME_MS + 1 + mClock.advanceTime(MIN_FGS_TIME_MS + 1); assertFalse(mExtender.shouldExtendLifetime(mEntry)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 57dd8c94c790..c66114b29ee1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -71,6 +71,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { @Mock private NotificationData mNotificationData; @Mock private DeviceProvisionedController mDeviceProvisionedController; @Mock private StatusBarKeyguardViewManager mKeyguardViewManager; + @Mock private NotificationClickNotifier mClickNotifier; private int mCurrentUserId; private TestNotificationLockscreenUserManager mLockscreenUserManager; @@ -185,7 +186,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { private class TestNotificationLockscreenUserManager extends NotificationLockscreenUserManagerImpl { public TestNotificationLockscreenUserManager(Context context) { - super(context); + super(context, mClickNotifier); } public BroadcastReceiver getBaseBroadcastReceiverForTest() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java index b81e04821463..fd91e4823a39 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java @@ -54,6 +54,7 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { @Mock private SmartReplyController mSmartReplyController; @Mock private NotificationListenerService.RankingMap mRanking; @Mock private ExpandableNotificationRow mRow; + @Mock private NotificationClickNotifier mClickNotifier; // Dependency mocks: @Mock private NotificationEntryManager mEntryManager; @@ -73,7 +74,8 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext, mLockscreenUserManager, mSmartReplyController, mEntryManager, () -> mock(ShadeController.class), - Handler.createAsync(Looper.myLooper())); + Handler.createAsync(Looper.myLooper()), + mClickNotifier); mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, new Notification(), UserHandle.CURRENT, null, 0); mEntry = new NotificationEntry(mSbn); @@ -202,9 +204,10 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { SmartReplyController smartReplyController, NotificationEntryManager notificationEntryManager, Lazy<ShadeController> shadeController, - Handler mainHandler) { + Handler mainHandler, + NotificationClickNotifier clickNotifier) { super(context, lockscreenUserManager, smartReplyController, notificationEntryManager, - shadeController, mainHandler); + shadeController, mainHandler, clickNotifier); } public void setUpWithPresenterForTest(Callback callback, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java index 81e373a8be27..8f1b6d3bb7f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java @@ -70,6 +70,7 @@ public class SmartReplyControllerTest extends SysuiTestCase { @Mock private StatusBarNotification mSbn; @Mock private NotificationEntryManager mNotificationEntryManager; @Mock private IStatusBarService mIStatusBarService; + @Mock private NotificationClickNotifier mClickNotifier; @Before public void setUp() { @@ -78,14 +79,15 @@ public class SmartReplyControllerTest extends SysuiTestCase { mNotificationEntryManager); mSmartReplyController = new SmartReplyController(mNotificationEntryManager, - mIStatusBarService); + mIStatusBarService, mClickNotifier); mDependency.injectTestDependency(SmartReplyController.class, mSmartReplyController); mRemoteInputManager = new NotificationRemoteInputManager(mContext, mock(NotificationLockscreenUserManager.class), mSmartReplyController, mNotificationEntryManager, () -> mock(ShadeController.class), - Handler.createAsync(Looper.myLooper())); + Handler.createAsync(Looper.myLooper()), + mClickNotifier); mRemoteInputManager.setUpWithCallback(mCallback, mDelegate); mNotification = new Notification.Builder(mContext, "") .setSmallIcon(R.drawable.ic_person) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java index f629757e4c68..76713e4015b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java @@ -54,6 +54,7 @@ import android.graphics.drawable.Icon; import android.media.session.MediaSession; import android.os.Bundle; import android.os.Process; +import android.os.SystemClock; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; @@ -339,7 +340,10 @@ public class NotificationDataTest extends SysuiTestCase { when(ranking.getSnoozeCriteria()).thenReturn(snoozeCriterions); NotificationEntry entry = - new NotificationEntry(mMockStatusBarNotification, ranking); + new NotificationEntry( + mMockStatusBarNotification, + ranking, + SystemClock.uptimeMillis()); assertEquals(systemGeneratedSmartActions, entry.systemGeneratedSmartActions); assertEquals(NOTIFICATION_CHANNEL, entry.channel); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index cca9f2834e93..258ddf3c98a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -30,6 +30,7 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -48,6 +49,7 @@ public class NotificationEntryTest extends SysuiTestCase { private NotificationEntry mEntry; private Bundle mExtras; + private final FakeSystemClock mClock = new FakeSystemClock(); @Before public void setUp() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 06d76ebcff28..8ec6a65d4c10 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -48,7 +48,6 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.internal.logging.MetricsLogger; -import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.ActivityIntentHelper; @@ -58,6 +57,7 @@ import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -94,7 +94,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { @Mock private ActivityStarter mActivityStarter; @Mock - private IStatusBarService mStatusBarService; + private NotificationClickNotifier mClickNotifier; @Mock private StatusBarStateController mStatusBarStateController; @Mock @@ -165,7 +165,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mNotificationActivityStarter = new StatusBarNotificationActivityStarter(getContext(), mock(CommandQueue.class), mAssistManager, mock(NotificationPanelView.class), mock(NotificationPresenter.class), mEntryManager, mock(HeadsUpManagerPhone.class), - mActivityStarter, mock(ActivityLaunchAnimator.class), mStatusBarService, + mActivityStarter, mock(ActivityLaunchAnimator.class), + mClickNotifier, mock(StatusBarStateController.class), mock(KeyguardManager.class), mock(IDreamManager.class), mRemoteInputManager, mock(StatusBarRemoteInputCallback.class), mock(NotificationGroupManager.class), @@ -222,7 +223,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mAssistManager).hideAssist(); - verify(mStatusBarService).onNotificationClick( + verify(mClickNotifier).onNotificationClick( eq(sbn.getKey()), any(NotificationVisibility.class)); // Notification is removed due to FLAG_AUTO_CANCEL @@ -248,7 +249,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mAssistManager).hideAssist(); - verify(mStatusBarService).onNotificationClick( + verify(mClickNotifier).onNotificationClick( eq(sbn.getKey()), any(NotificationVisibility.class)); // The content intent should NOT be sent on click. @@ -278,7 +279,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mAssistManager).hideAssist(); - verify(mStatusBarService).onNotificationClick( + verify(mClickNotifier).onNotificationClick( eq(sbn.getKey()), any(NotificationVisibility.class)); // The content intent should NOT be sent on click. @@ -308,7 +309,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mAssistManager).hideAssist(); - verify(mStatusBarService).onNotificationClick( + verify(mClickNotifier).onNotificationClick( eq(sbn.getKey()), any(NotificationVisibility.class)); // The content intent should NOT be sent on click. diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java new file mode 100644 index 000000000000..42e5a5eab6bf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import com.android.systemui.util.time.FakeSystemClock; + +import java.util.Collections; +import java.util.PriorityQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class FakeExecutor implements DelayableExecutor { + private final FakeSystemClock mClock; + private PriorityQueue<QueuedRunnable> mQueuedRunnables = new PriorityQueue<>(); + private boolean mIgnoreClockUpdates; + + /** + * Initializes a fake executor. + * + * @param clock FakeSystemClock allowing control over delayed runnables. It is strongly + * recommended that this clock have its auto-increment setting set to false to + * prevent unexpected advancement of the time. + */ + public FakeExecutor(FakeSystemClock clock) { + mClock = clock; + mClock.addListener(() -> { + if (!mIgnoreClockUpdates) { + runAllReady(); + } + }); + } + + /** + * Runs a single runnable if it's scheduled to run according to the internal clock. + * + * If constructed to advance the clock automatically, this will advance the clock enough to + * run the next pending item. + * + * This method does not advance the clock past the item that was run. + * + * @return Returns true if an item was run. + */ + public boolean runNextReady() { + if (!mQueuedRunnables.isEmpty() && mQueuedRunnables.peek().mWhen <= mClock.uptimeMillis()) { + mQueuedRunnables.poll().mRunnable.run(); + return true; + } + + return false; + } + + /** + * Runs all Runnables that are scheduled to run according to the internal clock. + * + * If constructed to advance the clock automatically, this will advance the clock enough to + * run all the pending items. This method does not advance the clock past items that were + * run. It is equivalent to calling {@link #runNextReady()} in a loop. + * + * @return Returns the number of items that ran. + */ + public int runAllReady() { + int num = 0; + while (runNextReady()) { + num++; + } + + return num; + } + + /** + * Advances the internal clock to the next item to run. + * + * The clock will only move forward. If the next item is set to run in the past or there is no + * next item, the clock does not change. + * + * Note that this will cause one or more items to actually run. + * + * @return The delta in uptimeMillis that the clock advanced, or 0 if the clock did not advance. + */ + public long advanceClockToNext() { + if (mQueuedRunnables.isEmpty()) { + return 0; + } + + long startTime = mClock.uptimeMillis(); + long nextTime = mQueuedRunnables.peek().mWhen; + if (nextTime <= startTime) { + return 0; + } + updateClock(nextTime); + + return nextTime - startTime; + } + + + /** + * Advances the internal clock to the last item to run. + * + * The clock will only move forward. If the last item is set to run in the past or there is no + * next item, the clock does not change. + * + * @return The delta in uptimeMillis that the clock advanced, or 0 if the clock did not advance. + */ + public long advanceClockToLast() { + if (mQueuedRunnables.isEmpty()) { + return 0; + } + + long startTime = mClock.uptimeMillis(); + long nextTime = Collections.max(mQueuedRunnables).mWhen; + if (nextTime <= startTime) { + return 0; + } + + updateClock(nextTime); + + return nextTime - startTime; + } + + /** + * Returns the number of un-executed runnables waiting to run. + */ + public int numPending() { + return mQueuedRunnables.size(); + } + + @Override + public Runnable executeDelayed(Runnable r, long delay, TimeUnit unit) { + if (delay < 0) { + delay = 0; + } + return executeAtTime(r, mClock.uptimeMillis() + unit.toMillis(delay)); + } + + @Override + public Runnable executeAtTime(Runnable r, long uptime, TimeUnit unit) { + long uptimeMillis = unit.toMillis(uptime); + + QueuedRunnable container = new QueuedRunnable(r, uptimeMillis); + + mQueuedRunnables.offer(container); + + return () -> mQueuedRunnables.remove(container); + } + + @Override + public void execute(Runnable command) { + executeDelayed(command, 0); + } + + /** + * Run all Executors in a loop until they all report they have no ready work to do. + * + * Useful if you have Executors the post work to other Executors, and you simply want to + * run them all until they stop posting work. + */ + public static void exhaustExecutors(FakeExecutor ...executors) { + boolean didAnything; + do { + didAnything = false; + for (FakeExecutor executor : executors) { + didAnything = didAnything || executor.runAllReady() != 0; + } + } while (didAnything); + } + + private void updateClock(long nextTime) { + mIgnoreClockUpdates = true; + mClock.setUptimeMillis(nextTime); + mIgnoreClockUpdates = false; + } + + private static class QueuedRunnable implements Comparable<QueuedRunnable> { + private static AtomicInteger sCounter = new AtomicInteger(); + + Runnable mRunnable; + long mWhen; + private int mCounter; + + private QueuedRunnable(Runnable r, long when) { + mRunnable = r; + mWhen = when; + + // PrioirityQueue orders items arbitrarily when equal. We want to ensure that + // otherwise-equal elements are ordered according to their insertion order. Because this + // class only is constructed right before insertion, we use a static counter to track + // insertion order of otherwise equal elements. + mCounter = sCounter.incrementAndGet(); + } + + @Override + public int compareTo(QueuedRunnable other) { + long diff = mWhen - other.mWhen; + + if (diff == 0) { + return mCounter - other.mCounter; + } + + return diff > 0 ? 1 : -1; + } + } +} + diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java b/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java new file mode 100644 index 000000000000..181636f4e7c8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.time; + +import com.android.systemui.util.concurrency.FakeExecutor; + +import java.util.ArrayList; +import java.util.List; + +/** + * A fake {@link SystemClock} for use with {@link FakeExecutor}. + * + * Attempts to simulate the behavior of a real system clock. Time can be moved forward but not + * backwards. uptimeMillis, elapsedRealtime, and currentThreadTimeMillis are all kept in sync. + * + * Unless otherwise specified, uptimeMillis and elapsedRealtime will advance the same amount with + * every call to {@link #advanceTime}. Thread time always lags by 50% of the uptime + * advancement to simulate time loss due to scheduling. + */ +public class FakeSystemClock implements SystemClock { + private long mUptimeMillis = 10000; + private long mElapsedRealtime = 10000; + private long mCurrentThreadTimeMillis = 10000; + private long mCurrentTimeMillis = 1555555500000L; + private final List<ClockTickListener> mListeners = new ArrayList<>(); + + @Override + public long uptimeMillis() { + return mUptimeMillis; + } + + @Override + public long elapsedRealtime() { + return mElapsedRealtime; + } + + @Override + public long elapsedRealtimeNanos() { + return mElapsedRealtime * 1000000 + 447; + } + + @Override + public long currentThreadTimeMillis() { + return mCurrentThreadTimeMillis; + } + + @Override + public long currentTimeMillis() { + return mCurrentTimeMillis; + } + + public void setUptimeMillis(long uptime) { + advanceTime(uptime - mUptimeMillis); + } + + public void setCurrentTimeMillis(long millis) { + mCurrentTimeMillis = millis; + } + + /** + * Advances the time tracked by the fake clock and notifies any listeners that the time has + * changed (for example, an attached {@link FakeExecutor} may fire its pending runnables). + * + * All tracked times increment by [millis], with the exception of currentThreadTimeMillis, + * which advances by [millis] * 0.5 + */ + public void advanceTime(long millis) { + advanceTime(millis, 0); + } + + /** + * Advances the time tracked by the fake clock and notifies any listeners that the time has + * changed (for example, an attached {@link FakeExecutor} may fire its pending runnables). + * + * The tracked times change as follows: + * - uptimeMillis increments by [awakeMillis] + * - currentThreadTimeMillis increments by [awakeMillis] * 0.5 + * - elapsedRealtime increments by [awakeMillis] + [sleepMillis] + * - currentTimeMillis increments by [awakeMillis] + [sleepMillis] + */ + public void advanceTime(long awakeMillis, long sleepMillis) { + if (awakeMillis < 0 || sleepMillis < 0) { + throw new IllegalArgumentException("Time cannot go backwards"); + } + + if (awakeMillis > 0 || sleepMillis > 0) { + mUptimeMillis += awakeMillis; + mElapsedRealtime += awakeMillis + sleepMillis; + mCurrentTimeMillis += awakeMillis + sleepMillis; + + mCurrentThreadTimeMillis += Math.ceil(awakeMillis * 0.5); + + for (ClockTickListener listener : mListeners) { + listener.onClockTick(); + } + } + } + + public void addListener(ClockTickListener listener) { + mListeners.add(listener); + } + + public void removeListener(ClockTickListener listener) { + mListeners.remove(listener); + } + + public interface ClockTickListener { + void onClockTick(); + } +} + |