Merge "Show correct last notification time for grouped notifications" into udc-dev

This commit is contained in:
Ibrahim Yilmaz
2023-03-07 22:03:11 +00:00
committed by Android (Google) Code Review
8 changed files with 426 additions and 25 deletions

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2023 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.statusbar.notification.collection.coordinator
import android.util.ArrayMap
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
import com.android.systemui.statusbar.notification.collection.render.NotifGroupController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
/** A small coordinator which finds, stores, and applies the closest notification time. */
@CoordinatorScope
class GroupWhenCoordinator
@Inject
constructor(
@Main private val delayableExecutor: DelayableExecutor,
private val systemClock: SystemClock
) : Coordinator {
private val invalidator = object : Invalidator("GroupWhenCoordinator") {}
private val notificationGroupTimes = ArrayMap<GroupEntry, Long>()
private var cancelInvalidateListRunnable: Runnable? = null
private val invalidateListRunnable: Runnable = Runnable {
invalidator.invalidateList("future notification invalidation")
}
override fun attach(pipeline: NotifPipeline) {
pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilterListener)
pipeline.addOnAfterRenderGroupListener(::onAfterRenderGroupListener)
pipeline.addPreRenderInvalidator(invalidator)
}
private fun onBeforeFinalizeFilterListener(entries: List<ListEntry>) {
cancelListInvalidation()
notificationGroupTimes.clear()
val now = systemClock.currentTimeMillis()
var closestFutureTime = Long.MAX_VALUE
entries.asSequence().filterIsInstance<GroupEntry>().forEach { groupEntry ->
val whenMillis = calculateGroupNotificationTime(groupEntry, now)
notificationGroupTimes[groupEntry] = whenMillis
if (whenMillis > now) {
closestFutureTime = min(closestFutureTime, whenMillis)
}
}
if (closestFutureTime != Long.MAX_VALUE) {
cancelInvalidateListRunnable =
delayableExecutor.executeDelayed(invalidateListRunnable, closestFutureTime - now)
}
}
private fun cancelListInvalidation() {
cancelInvalidateListRunnable?.run()
cancelInvalidateListRunnable = null
}
private fun onAfterRenderGroupListener(group: GroupEntry, controller: NotifGroupController) {
notificationGroupTimes[group]?.let(controller::setNotificationGroupWhen)
}
private fun calculateGroupNotificationTime(
groupEntry: GroupEntry,
currentTimeMillis: Long
): Long {
var pastTime = Long.MIN_VALUE
var futureTime = Long.MAX_VALUE
groupEntry.children
.asSequence()
.mapNotNull { child -> child.sbn.notification.`when`.takeIf { it > 0 } }
.forEach { time ->
val isInThePast = currentTimeMillis - time > 0
if (isInThePast) {
pastTime = max(pastTime, time)
} else {
futureTime = min(futureTime, time)
}
}
if (pastTime == Long.MIN_VALUE && futureTime == Long.MAX_VALUE) {
return checkNotNull(groupEntry.summary).creationTime
}
return if (futureTime != Long.MAX_VALUE) futureTime else pastTime
}
}

View File

@@ -31,31 +31,32 @@ interface NotifCoordinators : Coordinator, PipelineDumpable
@CoordinatorScope @CoordinatorScope
class NotifCoordinatorsImpl @Inject constructor( class NotifCoordinatorsImpl @Inject constructor(
notifPipelineFlags: NotifPipelineFlags, notifPipelineFlags: NotifPipelineFlags,
dataStoreCoordinator: DataStoreCoordinator, dataStoreCoordinator: DataStoreCoordinator,
hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator,
hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
keyguardCoordinator: KeyguardCoordinator, keyguardCoordinator: KeyguardCoordinator,
rankingCoordinator: RankingCoordinator, rankingCoordinator: RankingCoordinator,
appOpsCoordinator: AppOpsCoordinator, appOpsCoordinator: AppOpsCoordinator,
deviceProvisionedCoordinator: DeviceProvisionedCoordinator, deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
bubbleCoordinator: BubbleCoordinator, bubbleCoordinator: BubbleCoordinator,
headsUpCoordinator: HeadsUpCoordinator, headsUpCoordinator: HeadsUpCoordinator,
gutsCoordinator: GutsCoordinator, gutsCoordinator: GutsCoordinator,
conversationCoordinator: ConversationCoordinator, conversationCoordinator: ConversationCoordinator,
debugModeCoordinator: DebugModeCoordinator, debugModeCoordinator: DebugModeCoordinator,
groupCountCoordinator: GroupCountCoordinator, groupCountCoordinator: GroupCountCoordinator,
mediaCoordinator: MediaCoordinator, groupWhenCoordinator: GroupWhenCoordinator,
preparationCoordinator: PreparationCoordinator, mediaCoordinator: MediaCoordinator,
remoteInputCoordinator: RemoteInputCoordinator, preparationCoordinator: PreparationCoordinator,
rowAppearanceCoordinator: RowAppearanceCoordinator, remoteInputCoordinator: RemoteInputCoordinator,
stackCoordinator: StackCoordinator, rowAppearanceCoordinator: RowAppearanceCoordinator,
shadeEventCoordinator: ShadeEventCoordinator, stackCoordinator: StackCoordinator,
smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, shadeEventCoordinator: ShadeEventCoordinator,
viewConfigCoordinator: ViewConfigCoordinator, smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
visualStabilityCoordinator: VisualStabilityCoordinator, viewConfigCoordinator: ViewConfigCoordinator,
sensitiveContentCoordinator: SensitiveContentCoordinator, visualStabilityCoordinator: VisualStabilityCoordinator,
dismissibilityCoordinator: DismissibilityCoordinator sensitiveContentCoordinator: SensitiveContentCoordinator,
dismissibilityCoordinator: DismissibilityCoordinator
) : NotifCoordinators { ) : NotifCoordinators {
private val mCoordinators: MutableList<Coordinator> = ArrayList() private val mCoordinators: MutableList<Coordinator> = ArrayList()
@@ -82,6 +83,7 @@ class NotifCoordinatorsImpl @Inject constructor(
mCoordinators.add(debugModeCoordinator) mCoordinators.add(debugModeCoordinator)
mCoordinators.add(conversationCoordinator) mCoordinators.add(conversationCoordinator)
mCoordinators.add(groupCountCoordinator) mCoordinators.add(groupCountCoordinator)
mCoordinators.add(groupWhenCoordinator)
mCoordinators.add(mediaCoordinator) mCoordinators.add(mediaCoordinator)
mCoordinators.add(rowAppearanceCoordinator) mCoordinators.add(rowAppearanceCoordinator)
mCoordinators.add(stackCoordinator) mCoordinators.add(stackCoordinator)

View File

@@ -20,4 +20,7 @@ package com.android.systemui.statusbar.notification.collection.render
interface NotifGroupController { interface NotifGroupController {
/** Set the number of children that this group would have if not for the 8-child max */ /** Set the number of children that this group would have if not for the 8-child max */
fun setUntruncatedChildCount(untruncatedChildCount: Int) fun setUntruncatedChildCount(untruncatedChildCount: Int)
/** Set the when value of notification group that reflects most important closest notification time */
fun setNotificationGroupWhen(whenMillis: Long)
} }

View File

@@ -851,6 +851,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mChildrenContainer.setUntruncatedChildCount(childCount); mChildrenContainer.setUntruncatedChildCount(childCount);
} }
/**
* @see NotificationChildrenContainer#setNotificationGroupWhen(long)
*/
public void setNotificationGroupWhen(long whenMillis) {
if (mIsSummaryWithChildren) {
mChildrenContainer.setNotificationGroupWhen(whenMillis);
} else {
Log.w(TAG, "setNotificationGroupWhen( whenMillis: " + whenMillis + ")"
+ " mIsSummaryWithChildren: false"
+ " mChildrenContainer has not been inflated yet.");
}
}
/** /**
* Called after children have been attached to set the expansion states * Called after children have been attached to set the expansion states
*/ */

View File

@@ -348,6 +348,15 @@ public class ExpandableNotificationRowController implements NotifViewController
} }
} }
@Override
public void setNotificationGroupWhen(long whenMillis) {
if (mView.isSummaryWithChildren()) {
mView.setNotificationGroupWhen(whenMillis);
} else {
Log.w(TAG, "Called setNotificationTime(" + whenMillis + ") on a leaf row");
}
}
@Override @Override
public void setSystemExpanded(boolean systemExpanded) { public void setSystemExpanded(boolean systemExpanded) {
mView.setSystemExpanded(systemExpanded); mView.setSystemExpanded(systemExpanded);

View File

@@ -27,6 +27,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.Interpolator; import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator; import android.view.animation.PathInterpolator;
import android.widget.DateTimeView;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@@ -344,6 +345,21 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple
mTransformationHelper.setVisible(visible); mTransformationHelper.setVisible(visible);
} }
/***
* Set Notification when value
* @param whenMillis
*/
public void setNotificationWhen(long whenMillis) {
if (mNotificationHeader == null) {
return;
}
final View timeView = mNotificationHeader.findViewById(com.android.internal.R.id.time);
if (timeView instanceof DateTimeView) {
((DateTimeView) timeView).setTime(whenMillis);
}
}
protected void addTransformedViews(View... views) { protected void addTransformedViews(View... views) {
for (View view : views) { for (View view : views) {
if (view != null) { if (view != null) {

View File

@@ -295,6 +295,19 @@ public class NotificationChildrenContainer extends ViewGroup
updateGroupOverflow(); updateGroupOverflow();
} }
/**
* Set the notification time in the group so that the view can show the latest event in the UI
* appropriately.
*/
public void setNotificationGroupWhen(long whenMillis) {
if (mNotificationHeaderWrapper != null) {
mNotificationHeaderWrapper.setNotificationWhen(whenMillis);
}
if (mNotificationHeaderWrapperLowPriority != null) {
mNotificationHeaderWrapperLowPriority.setNotificationWhen(whenMillis);
}
}
/** /**
* Add a child notification to this view. * Add a child notification to this view.
* *

View File

@@ -0,0 +1,237 @@
/*
* Copyright (C) 2023 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.statusbar.notification.collection.coordinator
import android.app.Notification
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderGroupListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener
import com.android.systemui.statusbar.notification.collection.render.NotifGroupController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.SystemClock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.initMocks
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class GroupWhenCoordinatorTest : SysuiTestCase() {
private lateinit var beforeFinalizeFilterListener: OnBeforeFinalizeFilterListener
private lateinit var afterRenderGroupListener: OnAfterRenderGroupListener
@Mock private lateinit var pipeline: NotifPipeline
@Mock private lateinit var delayableExecutor: DelayableExecutor
@Mock private lateinit var groupController: NotifGroupController
@Mock private lateinit var systemClock: SystemClock
@InjectMocks private lateinit var coordinator: GroupWhenCoordinator
@Before
fun setUp() {
initMocks(this)
whenever(systemClock.currentTimeMillis()).thenReturn(NOW)
coordinator.attach(pipeline)
beforeFinalizeFilterListener = withArgCaptor {
verify(pipeline).addOnBeforeFinalizeFilterListener(capture())
}
afterRenderGroupListener = withArgCaptor {
verify(pipeline).addOnAfterRenderGroupListener(capture())
}
}
@Test
fun setNotificationGroupWhen_setClosestTimeByNow_whenAllNotificationsAreBeforeNow() {
// GIVEN
val summaryEntry = buildNotificationEntry(0, NOW)
val childEntry1 = buildNotificationEntry(1, NOW - 10L)
val childEntry2 = buildNotificationEntry(2, NOW - 100L)
val groupEntry =
GroupEntryBuilder()
.setSummary(summaryEntry)
.setChildren(listOf(childEntry1, childEntry2))
.build()
// WHEN
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController)
// THEN
verify(groupController).setNotificationGroupWhen(eq(NOW - 10L))
}
@Test
fun setNotificationGroupWhen_setClosestTimeByNow_whenAllNotificationsAreAfterNow() {
// GIVEN
val summaryEntry = buildNotificationEntry(0, NOW)
val childEntry1 = buildNotificationEntry(1, NOW + 10L)
val childEntry2 = buildNotificationEntry(2, NOW + 100L)
val groupEntry =
GroupEntryBuilder()
.setSummary(summaryEntry)
.setChildren(listOf(childEntry1, childEntry2))
.build()
// WHEN
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController)
// THEN
verify(groupController).setNotificationGroupWhen(eq(NOW + 10L))
}
@Test
fun setNotificationGroupWhen_setClosestFutureTimeByNow_whenThereAreBothBeforeAndAfterNow() {
// GIVEN
val summaryEntry = buildNotificationEntry(0, NOW)
val childEntry1 = buildNotificationEntry(1, NOW + 100L)
val childEntry2 = buildNotificationEntry(2, NOW + 10L)
val childEntry3 = buildNotificationEntry(3, NOW - 100L)
val childEntry4 = buildNotificationEntry(4, NOW - 9L)
val groupEntry =
GroupEntryBuilder()
.setSummary(summaryEntry)
.setChildren(listOf(childEntry1, childEntry2, childEntry3, childEntry4))
.build()
// WHEN
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController)
// THEN
verify(groupController).setNotificationGroupWhen(eq(NOW + 10L))
}
@Test
fun setNotificationGroupWhen_filterInvalidNotificationTimes() {
// GIVEN
val summaryEntry = buildNotificationEntry(0, NOW)
val childEntry1 = buildNotificationEntry(1, NOW + 100L)
val childEntry2 = buildNotificationEntry(2, -20000L)
val childEntry3 = buildNotificationEntry(4, 0)
val groupEntry =
GroupEntryBuilder()
.setSummary(summaryEntry)
.setChildren(listOf(childEntry1, childEntry2, childEntry3))
.build()
// WHEN
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController)
// THEN
verify(groupController).setNotificationGroupWhen(eq(NOW + 100))
}
@Test
fun setNotificationGroupWhen_setSummaryTimeWhenAllNotificationTimesAreInvalid() {
// GIVEN
val summaryEntry = buildNotificationEntry(0, NOW)
val childEntry1 = buildNotificationEntry(1, 0)
val childEntry2 = buildNotificationEntry(2, -1)
val groupEntry =
GroupEntryBuilder()
.setSummary(summaryEntry)
.setChildren(listOf(childEntry1, childEntry2))
.build()
// WHEN
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController)
// THEN
verify(groupController, never()).setNotificationGroupWhen(NOW)
}
@Test
fun setNotificationGroupWhen_schedulePipelineInvalidationWhenAnyNotificationIsInTheFuture() {
// GIVEN
val summaryEntry = buildNotificationEntry(0, NOW)
val childEntry1 = buildNotificationEntry(1, NOW + 1000L)
val childEntry2 = buildNotificationEntry(2, NOW + 2000L)
val childEntry3 = buildNotificationEntry(3, NOW - 100L)
val groupEntry =
GroupEntryBuilder()
.setSummary(summaryEntry)
.setChildren(listOf(childEntry1, childEntry2, childEntry3))
.build()
// WHEN
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController)
// THEN
verify(delayableExecutor).executeDelayed(any(), eq(1000))
}
@Test
fun setNotificationGroupWhen_cancelPrevPipelineInvalidation() {
// GIVEN
val summaryEntry = buildNotificationEntry(0, NOW)
val childEntry1 = buildNotificationEntry(1, NOW + 1L)
val prevInvalidation = mock<Runnable>()
whenever(delayableExecutor.executeDelayed(any(), any())).thenReturn(prevInvalidation)
val groupEntry =
GroupEntryBuilder().setSummary(summaryEntry).setChildren(listOf(childEntry1)).build()
// WHEN
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController)
beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
// THEN
verify(prevInvalidation).run()
}
private fun buildNotificationEntry(id: Int, timeMillis: Long): NotificationEntry {
val notification = Notification.Builder(mContext).setWhen(timeMillis).build()
val sbn = SbnBuilder().setNotification(notification).build()
return NotificationEntryBuilder().setId(id).setSbn(sbn).build()
}
private companion object {
private const val NOW = 1000L
}
}