diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHub.kt index e81d361f58f7f..2a02c199ee5c2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHub.kt @@ -19,7 +19,13 @@ package com.android.systemui.statusbar.notification.people import android.app.PendingIntent import android.graphics.drawable.Drawable -/** `ViewModel` for PeopleHub view. */ +/** + * `ViewModel` for PeopleHub view. + * + * @param people ViewModels for individual people in PeopleHub, in order that they should be + * displayed + * @param isVisible Whether or not the whole PeopleHub UI is visible + **/ data class PeopleHubViewModel(val people: Sequence, val isVisible: Boolean) /** `ViewModel` for a single "Person' in PeopleHub. */ @@ -29,7 +35,11 @@ data class PersonViewModel( val onClick: () -> Unit ) -/** `Model` for PeopleHub. */ +/** + * `Model` for PeopleHub. + * + * @param people Models for individual people in PeopleHub, in order that they should be displayed + **/ data class PeopleHubModel(val people: Collection) /** `Model` for a single "Person" in PeopleHub. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt index fc221d43b3dde..721f32a7ed155 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt @@ -195,7 +195,10 @@ private fun NotificationLockscreenUserManager.registerListener( class PeopleHubManager { + // People currently visible in the notification shade, and so are not in the hub private val activePeople = mutableMapOf() + + // People that were once "active" and have been dismissed, and so can be displayed in the hub private val inactivePeople = ArrayDeque(MAX_STORED_INACTIVE_PEOPLE) fun removeActivePerson(key: PersonKey): Boolean { @@ -203,7 +206,7 @@ class PeopleHubManager { if (inactivePeople.size >= MAX_STORED_INACTIVE_PEOPLE) { inactivePeople.removeLast() } - inactivePeople.add(data) + inactivePeople.addFirst(data) return true } return false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt index e28d03fc8b421..a58c42b839cf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt @@ -38,7 +38,14 @@ interface PeopleHubViewBoundary { /** View used for animating the activity launch caused by clicking a person in the hub. */ val associatedViewForClickAnimation: View - /** [DataListener]s for individual people in the hub. */ + /** + * [DataListener]s for individual people in the hub. + * + * These listeners should be ordered such that the first element will be bound to the most + * recent person to be added to the hub, and then continuing in descending order. If there are + * not enough people to satisfy each listener, `null` will be passed instead, indicating that + * the `View` should render a placeholder. + */ val personViewAdapters: Sequence> /** Sets the visibility of the Hub in the notification shade. */ @@ -80,8 +87,8 @@ private class PeopleHubDataListenerImpl( ) viewBoundary.setVisible(viewModel.isVisible) val padded = viewModel.people + repeated(null) - for ((personAdapter, personModel) in viewBoundary.personViewAdapters.zip(padded)) { - personAdapter.onDataChanged(personModel) + for ((adapter, model) in viewBoundary.personViewAdapters.zip(padded)) { + adapter.onDataChanged(model) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt index 151c6b272a3e9..0708f766738ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt @@ -38,13 +38,13 @@ class PeopleHubView(context: Context, attrs: AttributeSet) : override fun onFinishInflate() { contents = requireViewById(R.id.people_list) personViewAdapters = (0 until contents.childCount) - .reversed() - .asSequence() + .asSequence() // so we can map .mapNotNull { idx -> + // get all our people slots (contents.getChildAt(idx) as? ImageView)?.let(::PersonDataListenerImpl) } - .toList() - .asSequence() + .toList() // cache it + .asSequence() // but don't reveal it's a list super.onFinishInflate() setVisible(true /* nowVisible */, false /* animate */) } @@ -80,6 +80,7 @@ class PeopleHubView(context: Context, attrs: AttributeSet) : DataListener { override fun onDataChanged(data: PersonViewModel?) { + avatarView.visibility = data?.let { View.VISIBLE } ?: View.GONE avatarView.setImageDrawable(data?.icon) avatarView.setOnClickListener { data?.onClick?.invoke() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleFakes.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleFakes.kt new file mode 100644 index 0000000000000..1fb4ca1ac5db6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleFakes.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 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.people + +object EmptySubscription : Subscription { + override fun unsubscribe() {} +} + +class FakeDataSource( + private val data: T, + private val subscription: Subscription = EmptySubscription +) : DataSource { + override fun registerListener(listener: DataListener): Subscription { + listener.onDataChanged(data) + return subscription + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleHubViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleHubViewControllerTest.kt index abce8b517dfbc..b0ca943bd6bec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleHubViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleHubViewControllerTest.kt @@ -30,12 +30,12 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import kotlin.reflect.KClass +import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) @@ -50,24 +50,19 @@ class PeopleHubViewControllerTest : SysuiTestCase() { fun testBindViewModelToViewBoundary() { val fakePerson1 = fakePersonViewModel("name") val fakeViewModel = PeopleHubViewModel(sequenceOf(fakePerson1), true) + + val mockFactory = mock(PeopleHubViewModelFactory::class.java) + whenever(mockFactory.createWithAssociatedClickView(any())).thenReturn(fakeViewModel) + + val mockClickView = mock(View::class.java) + whenever(mockViewBoundary.associatedViewForClickAnimation).thenReturn(mockClickView) + val fakePersonViewAdapter1 = FakeDataListener() val fakePersonViewAdapter2 = FakeDataListener() - val mockClickView = mock(View::class.java) - `when`(mockViewBoundary.associatedViewForClickAnimation).thenReturn(mockClickView) - `when`(mockViewBoundary.personViewAdapters) + whenever(mockViewBoundary.personViewAdapters) .thenReturn(sequenceOf(fakePersonViewAdapter1, fakePersonViewAdapter2)) - val mockFactory = mock(PeopleHubViewModelFactory::class.java) - `when`(mockFactory.createWithAssociatedClickView(any())).thenReturn(fakeViewModel) - val mockSubscription = mock(Subscription::class.java) - val fakeFactoryDataSource = object : DataSource { - override fun registerListener( - listener: DataListener - ): Subscription { - listener.onDataChanged(mockFactory) - return mockSubscription - } - } - val adapter = PeopleHubViewAdapterImpl(fakeFactoryDataSource) + + val adapter = PeopleHubViewAdapterImpl(FakeDataSource(mockFactory)) adapter.bindView(mockViewBoundary) @@ -77,24 +72,40 @@ class PeopleHubViewControllerTest : SysuiTestCase() { verify(mockFactory).createWithAssociatedClickView(mockClickView) } + @Test + fun testBindViewModelToViewBoundary_moreDataThanCanBeDisplayed_displaysMostRecent() { + val fakePerson1 = fakePersonViewModel("person1") + val fakePerson2 = fakePersonViewModel("person2") + val fakePerson3 = fakePersonViewModel("person3") + val fakePeople = sequenceOf(fakePerson3, fakePerson2, fakePerson1) + val fakeViewModel = PeopleHubViewModel(fakePeople, true) + + val mockFactory = mock(PeopleHubViewModelFactory::class.java) + whenever(mockFactory.createWithAssociatedClickView(any())).thenReturn(fakeViewModel) + + whenever(mockViewBoundary.associatedViewForClickAnimation) + .thenReturn(mock(View::class.java)) + + val fakePersonViewAdapter1 = FakeDataListener() + val fakePersonViewAdapter2 = FakeDataListener() + whenever(mockViewBoundary.personViewAdapters) + .thenReturn(sequenceOf(fakePersonViewAdapter1, fakePersonViewAdapter2)) + + val adapter = PeopleHubViewAdapterImpl(FakeDataSource(mockFactory)) + + adapter.bindView(mockViewBoundary) + + assertThat(fakePersonViewAdapter1.lastSeen).isEqualTo(Maybe.Just(fakePerson3)) + assertThat(fakePersonViewAdapter2.lastSeen).isEqualTo(Maybe.Just(fakePerson2)) + } + @Test fun testViewModelDataSourceTransformsModel() { val fakeClickIntent = PendingIntent.getActivity(context, 0, Intent("action"), 0) val fakePerson = fakePersonModel("id", "name", fakeClickIntent) val fakeModel = PeopleHubModel(listOf(fakePerson)) - val mockSubscription = mock(Subscription::class.java) - val fakeModelDataSource = object : DataSource { - override fun registerListener(listener: DataListener): Subscription { - listener.onDataChanged(fakeModel) - return mockSubscription - } - } - val fakeSettingDataSource = object : DataSource { - override fun registerListener(listener: DataListener): Subscription { - listener.onDataChanged(true) - return mockSubscription - } - } + val fakeModelDataSource = FakeDataSource(fakeModel) + val fakeSettingDataSource = FakeDataSource(true) val factoryDataSource = PeopleHubViewModelFactoryDataSourceImpl( mockActivityStarter, fakeModelDataSource, @@ -127,19 +138,8 @@ class PeopleHubViewControllerTest : SysuiTestCase() { val fakeClickIntent = PendingIntent.getActivity(context, 0, Intent("action"), 0) val fakePerson = fakePersonModel("id", "name", fakeClickIntent) val fakeModel = PeopleHubModel(listOf(fakePerson)) - val mockSubscription = mock(Subscription::class.java) - val fakeModelDataSource = object : DataSource { - override fun registerListener(listener: DataListener): Subscription { - listener.onDataChanged(fakeModel) - return mockSubscription - } - } - val fakeSettingDataSource = object : DataSource { - override fun registerListener(listener: DataListener): Subscription { - listener.onDataChanged(false) - return mockSubscription - } - } + val fakeModelDataSource = FakeDataSource(fakeModel) + val fakeSettingDataSource = FakeDataSource(false) val factoryDataSource = PeopleHubViewModelFactoryDataSourceImpl( mockActivityStarter, fakeModelDataSource,