Delete unused PeopleHub code

Bug: 200269355
Test: atest SystemUITests
Change-Id: I9f045803bad4c4e09db5fd4ed69651b46ab7a188
This commit is contained in:
Steve Elliott
2022-08-25 00:15:16 -04:00
parent 28ea00f4ab
commit 207bcfaf59
10 changed files with 50 additions and 1008 deletions

View File

@@ -1,102 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<com.android.systemui.statusbar.notification.stack.PeopleHubView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_section_header_height"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:focusable="true"
android:clickable="true"
>
<LinearLayout
android:id="@+id/people_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:gravity="bottom"
android:orientation="horizontal"
android:forceHasOverlappingRendering="false"
android:clipChildren="false">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:layout_weight="1"
android:forceHasOverlappingRendering="false">
<TextView
android:id="@+id/header_label"
style="@style/TextAppearance.NotificationSectionHeaderButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/notification_section_header_conversations"
/>
</FrameLayout>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:scaleType="fitCenter"
android:forceHasOverlappingRendering="false"
android:visibility="gone"
/>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:scaleType="fitCenter"
android:forceHasOverlappingRendering="false"
android:visibility="gone"
/>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:scaleType="fitCenter"
android:forceHasOverlappingRendering="false"
android:visibility="gone"
/>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:scaleType="fitCenter"
android:forceHasOverlappingRendering="false"
android:visibility="gone"
/>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:scaleType="fitCenter"
android:forceHasOverlappingRendering="false"
android:visibility="gone"
/>
</LinearLayout>
</com.android.systemui.statusbar.notification.stack.PeopleHubView>

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2022 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
import android.service.notification.StatusBarNotification
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.plugins.NotificationPersonExtractorPlugin
import com.android.systemui.statusbar.policy.ExtensionController
import javax.inject.Inject
interface NotificationPersonExtractor {
fun isPersonNotification(sbn: StatusBarNotification): Boolean
}
@SysUISingleton
class NotificationPersonExtractorPluginBoundary @Inject constructor(
extensionController: ExtensionController
) : NotificationPersonExtractor {
private var plugin: NotificationPersonExtractorPlugin? = null
init {
plugin = extensionController
.newExtension(NotificationPersonExtractorPlugin::class.java)
.withPlugin(NotificationPersonExtractorPlugin::class.java)
.withCallback { extractor ->
plugin = extractor
}
.build()
.get()
}
override fun isPersonNotification(sbn: StatusBarNotification): Boolean =
plugin?.isPersonNotification(sbn) ?: false
}

View File

@@ -1,55 +0,0 @@
/*
* 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.statusbar.notification.people
import android.graphics.drawable.Drawable
/**
* `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<PersonViewModel>, val isVisible: Boolean)
/** `ViewModel` for a single "Person' in PeopleHub. */
data class PersonViewModel(
val name: CharSequence,
val icon: Drawable,
val onClick: () -> Unit
)
/**
* `Model` for PeopleHub.
*
* @param people Models for individual people in PeopleHub, in order that they should be displayed
**/
data class PeopleHubModel(val people: Collection<PersonModel>)
/** `Model` for a single "Person" in PeopleHub. */
data class PersonModel(
val key: PersonKey,
val userId: Int,
// TODO: these should live in the ViewModel
val name: CharSequence,
val avatar: Drawable,
val clickRunnable: Runnable
)
/** Unique identifier for a Person in PeopleHub. */
typealias PersonKey = String

View File

@@ -21,25 +21,6 @@ import dagger.Module
@Module
abstract class PeopleHubModule {
@Binds
abstract fun peopleHubSectionFooterViewAdapter(
impl: PeopleHubViewAdapterImpl
): PeopleHubViewAdapter
@Binds
abstract fun peopleHubDataSource(impl: PeopleHubDataSourceImpl): DataSource<PeopleHubModel>
@Binds
abstract fun peopleHubSettingChangeDataSource(
impl: PeopleHubSettingChangeDataSourceImpl
): DataSource<Boolean>
@Binds
abstract fun peopleHubViewModelFactoryDataSource(
impl: PeopleHubViewModelFactoryDataSourceImpl
): DataSource<PeopleHubViewModelFactory>
@Binds
abstract fun peopleNotificationIdentifier(
impl: PeopleNotificationIdentifierImpl
@@ -49,4 +30,4 @@ abstract class PeopleHubModule {
abstract fun notificationPersonExtractor(
pluginImpl: NotificationPersonExtractorPluginBoundary
): NotificationPersonExtractor
}
}

View File

@@ -1,315 +0,0 @@
/*
* 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.statusbar.notification.people
import android.app.Notification
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.content.pm.UserInfo
import android.graphics.drawable.Drawable
import android.os.UserManager
import android.service.notification.NotificationListenerService
import android.service.notification.NotificationListenerService.REASON_SNOOZED
import android.service.notification.StatusBarNotification
import android.util.IconDrawableFactory
import android.util.SparseArray
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.android.internal.widget.MessagingGroup
import com.android.settingslib.notification.ConversationIconFactory
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.NotificationPersonExtractorPlugin
import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
import com.android.systemui.statusbar.policy.ExtensionController
import java.util.ArrayDeque
import java.util.concurrent.Executor
import javax.inject.Inject
private const val MAX_STORED_INACTIVE_PEOPLE = 10
interface NotificationPersonExtractor {
fun extractPerson(sbn: StatusBarNotification): PersonModel?
fun extractPersonKey(sbn: StatusBarNotification): String?
fun isPersonNotification(sbn: StatusBarNotification): Boolean
}
@SysUISingleton
class NotificationPersonExtractorPluginBoundary @Inject constructor(
extensionController: ExtensionController
) : NotificationPersonExtractor {
private var plugin: NotificationPersonExtractorPlugin? = null
init {
plugin = extensionController
.newExtension(NotificationPersonExtractorPlugin::class.java)
.withPlugin(NotificationPersonExtractorPlugin::class.java)
.withCallback { extractor ->
plugin = extractor
}
.build()
.get()
}
override fun extractPerson(sbn: StatusBarNotification) =
plugin?.extractPerson(sbn)?.run {
PersonModel(key, sbn.user.identifier, name, avatar, clickRunnable)
}
override fun extractPersonKey(sbn: StatusBarNotification) = plugin?.extractPersonKey(sbn)
override fun isPersonNotification(sbn: StatusBarNotification): Boolean =
plugin?.isPersonNotification(sbn) ?: false
}
@SysUISingleton
class PeopleHubDataSourceImpl @Inject constructor(
private val notifCollection: CommonNotifCollection,
private val extractor: NotificationPersonExtractor,
private val userManager: UserManager,
launcherApps: LauncherApps,
packageManager: PackageManager,
context: Context,
private val notificationListener: NotificationListener,
@Background private val bgExecutor: Executor,
@Main private val mainExecutor: Executor,
private val notifLockscreenUserMgr: NotificationLockscreenUserManager,
private val peopleNotificationIdentifier: PeopleNotificationIdentifier
) : DataSource<PeopleHubModel> {
private var userChangeSubscription: Subscription? = null
private val dataListeners = mutableListOf<DataListener<PeopleHubModel>>()
private val peopleHubManagerForUser = SparseArray<PeopleHubManager>()
private val iconFactory = run {
val appContext = context.applicationContext
ConversationIconFactory(
appContext,
launcherApps,
packageManager,
IconDrawableFactory.newInstance(appContext),
appContext.resources.getDimensionPixelSize(
R.dimen.notification_guts_conversation_icon_size
)
)
}
private val notifCollectionListener = object : NotifCollectionListener {
override fun onEntryAdded(entry: NotificationEntry) = addVisibleEntry(entry)
override fun onEntryUpdated(entry: NotificationEntry) = addVisibleEntry(entry)
override fun onEntryRemoved(entry: NotificationEntry, reason: Int) =
removeVisibleEntry(entry, reason)
}
private fun removeVisibleEntry(entry: NotificationEntry, reason: Int) {
(extractor.extractPersonKey(entry.sbn) ?: entry.extractPersonKey())?.let { key ->
val userId = entry.sbn.user.identifier
bgExecutor.execute {
val parentId = userManager.getProfileParent(userId)?.id ?: userId
mainExecutor.execute {
if (reason == REASON_SNOOZED) {
if (peopleHubManagerForUser[parentId]?.migrateActivePerson(key) == true) {
updateUi()
}
} else {
peopleHubManagerForUser[parentId]?.removeActivePerson(key)
}
}
}
}
}
private fun addVisibleEntry(entry: NotificationEntry) {
entry.extractPerson()?.let { personModel ->
val userId = entry.sbn.user.identifier
bgExecutor.execute {
val parentId = userManager.getProfileParent(userId)?.id ?: userId
mainExecutor.execute {
val manager = peopleHubManagerForUser[parentId]
?: PeopleHubManager().also { peopleHubManagerForUser.put(parentId, it) }
if (manager.addActivePerson(personModel)) {
updateUi()
}
}
}
}
}
override fun registerListener(listener: DataListener<PeopleHubModel>): Subscription {
val register = dataListeners.isEmpty()
dataListeners.add(listener)
if (register) {
userChangeSubscription = notifLockscreenUserMgr.registerListener(
object : NotificationLockscreenUserManager.UserChangedListener {
override fun onUserChanged(userId: Int) = updateUi()
override fun onCurrentProfilesChanged(
currentProfiles: SparseArray<UserInfo>?
) = updateUi()
})
notifCollection.addCollectionListener(notifCollectionListener)
} else {
getPeopleHubModelForCurrentUser()?.let(listener::onDataChanged)
}
return object : Subscription {
override fun unsubscribe() {
dataListeners.remove(listener)
if (dataListeners.isEmpty()) {
userChangeSubscription?.unsubscribe()
userChangeSubscription = null
notifCollection.removeCollectionListener(notifCollectionListener)
}
}
}
}
private fun getPeopleHubModelForCurrentUser(): PeopleHubModel? {
val currentUserId = notifLockscreenUserMgr.currentUserId
val model = peopleHubManagerForUser[currentUserId]?.getPeopleHubModel()
?: return null
val currentProfiles = notifLockscreenUserMgr.currentProfiles
return model.copy(people = model.people.filter { person ->
currentProfiles[person.userId]?.isQuietModeEnabled == false
})
}
private fun updateUi() {
val model = getPeopleHubModelForCurrentUser() ?: return
for (listener in dataListeners) {
listener.onDataChanged(model)
}
}
private fun NotificationEntry.extractPerson(): PersonModel? {
val type = peopleNotificationIdentifier.getPeopleNotificationType(this)
if (type == TYPE_NON_PERSON) {
return null
}
val clickRunnable = Runnable { notificationListener.unsnoozeNotification(key) }
val extras = sbn.notification.extras
val name = ranking.conversationShortcutInfo?.label
?: extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE)
?: extras.getCharSequence(Notification.EXTRA_TITLE)
?: return null
val drawable = ranking.getIcon(iconFactory, sbn)
?: iconFactory.getConversationDrawable(
extractAvatarFromRow(this),
sbn.packageName,
sbn.uid,
ranking.channel.isImportantConversation
)
return PersonModel(key, sbn.user.identifier, name, drawable, clickRunnable)
}
private fun NotificationListenerService.Ranking.getIcon(
iconFactory: ConversationIconFactory,
sbn: StatusBarNotification
): Drawable? =
conversationShortcutInfo?.let { conversationShortcutInfo ->
iconFactory.getConversationDrawable(
conversationShortcutInfo,
sbn.packageName,
sbn.uid,
channel.isImportantConversation
)
}
private fun NotificationEntry.extractPersonKey(): PersonKey? {
// TODO migrate to shortcut id when snoozing is conversation wide
val type = peopleNotificationIdentifier.getPeopleNotificationType(this)
return if (type != TYPE_NON_PERSON) key else null
}
}
private fun NotificationLockscreenUserManager.registerListener(
listener: NotificationLockscreenUserManager.UserChangedListener
): Subscription {
addUserChangedListener(listener)
return object : Subscription {
override fun unsubscribe() {
removeUserChangedListener(listener)
}
}
}
class PeopleHubManager {
// People currently visible in the notification shade, and so are not in the hub
private val activePeople = mutableMapOf<PersonKey, PersonModel>()
// People that were once "active" and have been dismissed, and so can be displayed in the hub
private val inactivePeople = ArrayDeque<PersonModel>(MAX_STORED_INACTIVE_PEOPLE)
fun migrateActivePerson(key: PersonKey): Boolean {
activePeople.remove(key)?.let { data ->
if (inactivePeople.size >= MAX_STORED_INACTIVE_PEOPLE) {
inactivePeople.removeLast()
}
inactivePeople.addFirst(data)
return true
}
return false
}
fun removeActivePerson(key: PersonKey) {
activePeople.remove(key)
}
fun addActivePerson(person: PersonModel): Boolean {
activePeople[person.key] = person
return inactivePeople.removeIf { it.key == person.key }
}
fun getPeopleHubModel(): PeopleHubModel = PeopleHubModel(inactivePeople)
}
private val ViewGroup.children
get(): Sequence<View> = sequence {
for (i in 0 until childCount) {
yield(getChildAt(i))
}
}
private fun ViewGroup.childrenWithId(id: Int): Sequence<View> = children.filter { it.id == id }
fun extractAvatarFromRow(entry: NotificationEntry): Drawable? =
entry.row
?.childrenWithId(R.id.expanded)
?.mapNotNull { it as? ViewGroup }
?.flatMap {
it.childrenWithId(com.android.internal.R.id.status_bar_latest_event_content)
}
?.mapNotNull {
it.findViewById<ViewGroup>(com.android.internal.R.id.notification_messaging)
}
?.mapNotNull { messagesView ->
messagesView.children
.mapNotNull { it as? MessagingGroup }
.lastOrNull()
?.findViewById<ImageView>(com.android.internal.R.id.message_icon)
?.drawable
}
?.firstOrNull()

View File

@@ -1,193 +0,0 @@
/*
* 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.statusbar.notification.people
import android.content.Context
import android.database.ContentObserver
import android.net.Uri
import android.os.Handler
import android.os.UserHandle
import android.provider.Settings
import android.view.View
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import javax.inject.Inject
/** Boundary between the View and PeopleHub, as seen by the View. */
interface PeopleHubViewAdapter {
fun bindView(viewBoundary: PeopleHubViewBoundary): Subscription
}
/** Abstract `View` representation of PeopleHub. */
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.
*
* 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<DataListener<PersonViewModel?>>
/** Sets the visibility of the Hub in the notification shade. */
fun setVisible(isVisible: Boolean)
}
/** Creates a [PeopleHubViewModel] given some additional information required from the `View`. */
interface PeopleHubViewModelFactory {
/**
* Creates a [PeopleHubViewModel] that, when clicked, starts an activity using an animation
* involving the given [view].
*/
fun createWithAssociatedClickView(view: View): PeopleHubViewModel
}
/**
* Wraps a [PeopleHubViewBoundary] in a [DataListener], and connects it to the data
* pipeline.
*
* @param dataSource PeopleHub data pipeline.
*/
@SysUISingleton
class PeopleHubViewAdapterImpl @Inject constructor(
private val dataSource: DataSource<@JvmSuppressWildcards PeopleHubViewModelFactory>
) : PeopleHubViewAdapter {
override fun bindView(viewBoundary: PeopleHubViewBoundary): Subscription =
dataSource.registerListener(PeopleHubDataListenerImpl(viewBoundary))
}
private class PeopleHubDataListenerImpl(
private val viewBoundary: PeopleHubViewBoundary
) : DataListener<PeopleHubViewModelFactory> {
override fun onDataChanged(data: PeopleHubViewModelFactory) {
val viewModel = data.createWithAssociatedClickView(
viewBoundary.associatedViewForClickAnimation
)
viewBoundary.setVisible(viewModel.isVisible)
val padded = viewModel.people + repeated(null)
for ((adapter, model) in viewBoundary.personViewAdapters.zip(padded)) {
adapter.onDataChanged(model)
}
}
}
/**
* Converts [PeopleHubModel]s into [PeopleHubViewModelFactory]s.
*
* This class serves as the glue between the View layer (which depends on
* [PeopleHubViewBoundary]) and the Data layer (which produces [PeopleHubModel]s).
*/
@SysUISingleton
class PeopleHubViewModelFactoryDataSourceImpl @Inject constructor(
private val activityStarter: ActivityStarter,
private val dataSource: DataSource<@JvmSuppressWildcards PeopleHubModel>
) : DataSource<PeopleHubViewModelFactory> {
override fun registerListener(listener: DataListener<PeopleHubViewModelFactory>): Subscription {
var model: PeopleHubModel? = null
fun updateListener() {
// don't invoke listener until we've received our first model
model?.let { model ->
val factory = PeopleHubViewModelFactoryImpl(model, activityStarter)
listener.onDataChanged(factory)
}
}
val dataSub = dataSource.registerListener(object : DataListener<PeopleHubModel> {
override fun onDataChanged(data: PeopleHubModel) {
model = data
updateListener()
}
})
return object : Subscription {
override fun unsubscribe() {
dataSub.unsubscribe()
}
}
}
}
private object EmptyViewModelFactory : PeopleHubViewModelFactory {
override fun createWithAssociatedClickView(view: View): PeopleHubViewModel {
return PeopleHubViewModel(emptySequence(), false)
}
}
private class PeopleHubViewModelFactoryImpl(
private val model: PeopleHubModel,
private val activityStarter: ActivityStarter
) : PeopleHubViewModelFactory {
override fun createWithAssociatedClickView(view: View): PeopleHubViewModel {
val personViewModels = model.people.asSequence().map { personModel ->
val onClick = {
personModel.clickRunnable.run()
}
PersonViewModel(personModel.name, personModel.avatar, onClick)
}
return PeopleHubViewModel(personViewModels, model.people.isNotEmpty())
}
}
@SysUISingleton
class PeopleHubSettingChangeDataSourceImpl @Inject constructor(
@Main private val handler: Handler,
context: Context
) : DataSource<Boolean> {
private val settingUri = Settings.Secure.getUriFor(Settings.Secure.PEOPLE_STRIP)
private val contentResolver = context.contentResolver
override fun registerListener(listener: DataListener<Boolean>): Subscription {
// Immediately report current value of setting
updateListener(listener)
val observer = object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?, flags: Int) {
super.onChange(selfChange, uri, flags)
updateListener(listener)
}
}
contentResolver.registerContentObserver(settingUri, false, observer, UserHandle.USER_ALL)
return object : Subscription {
override fun unsubscribe() = contentResolver.unregisterContentObserver(observer)
}
}
private fun updateListener(listener: DataListener<Boolean>) {
val setting = Settings.Secure.getIntForUser(
contentResolver,
Settings.Secure.PEOPLE_STRIP,
0,
UserHandle.USER_CURRENT
)
listener.onDataChanged(setting != 0)
}
}
private fun <T> repeated(value: T): Sequence<T> = sequence {
while (true) {
yield(value)
}
}

View File

@@ -6116,9 +6116,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
return row.canViewBeDismissed();
}
if (v instanceof PeopleHubView) {
return ((PeopleHubView) v).getCanSwipe();
}
return false;
}
@@ -6130,9 +6127,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
return row.canViewBeCleared();
}
if (v instanceof PeopleHubView) {
return ((PeopleHubView) v).getCanSwipe();
}
return false;
}

View File

@@ -1,107 +0,0 @@
/*
* 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.statusbar.notification.stack
import android.annotation.ColorInt
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin
import com.android.systemui.statusbar.notification.people.DataListener
import com.android.systemui.statusbar.notification.people.PersonViewModel
import com.android.systemui.statusbar.notification.row.StackScrollerDecorView
class PeopleHubView(context: Context, attrs: AttributeSet) :
StackScrollerDecorView(context, attrs), SwipeableView {
private lateinit var contents: ViewGroup
private lateinit var label: TextView
lateinit var personViewAdapters: Sequence<DataListener<PersonViewModel?>>
private set
override fun onFinishInflate() {
contents = requireViewById(R.id.people_list)
label = requireViewById(R.id.header_label)
personViewAdapters = (0 until contents.childCount)
.asSequence() // so we can map
.mapNotNull { idx ->
// get all our people slots
(contents.getChildAt(idx) as? ImageView)?.let(::PersonDataListenerImpl)
}
.toList() // cache it
.asSequence() // but don't reveal it's a list
super.onFinishInflate()
setVisible(true /* nowVisible */, false /* animate */)
}
fun setTextColor(@ColorInt color: Int) = label.setTextColor(color)
override fun findContentView(): View = contents
override fun findSecondaryView(): View? = null
override fun hasFinishedInitialization(): Boolean = true
override fun createMenu(): NotificationMenuRowPlugin? = null
override fun resetTranslation() {
translationX = 0f
}
override fun setTranslation(translation: Float) {
if (canSwipe) {
super.setTranslation(translation)
}
}
var canSwipe: Boolean = false
set(value) {
if (field != value) {
if (field) {
resetTranslation()
}
field = value
}
}
override fun needsClippingToShelf(): Boolean = true
override fun applyContentTransformation(contentAlpha: Float, translationY: Float) {
super.applyContentTransformation(contentAlpha, translationY)
for (i in 0 until contents.childCount) {
val view = contents.getChildAt(i)
view.alpha = contentAlpha
view.translationY = translationY
}
}
fun setOnHeaderClickListener(listener: OnClickListener) = label.setOnClickListener(listener)
private inner class PersonDataListenerImpl(val avatarView: ImageView) :
DataListener<PersonViewModel?> {
override fun onDataChanged(data: PersonViewModel?) {
avatarView.visibility = data?.let { View.VISIBLE } ?: View.GONE
avatarView.setImageDrawable(data?.icon)
avatarView.setOnClickListener { data?.onClick?.invoke() }
}
}
}

View File

@@ -1,31 +0,0 @@
/*
* 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<T>(
private val data: T,
private val subscription: Subscription = EmptySubscription
) : DataSource<T> {
override fun registerListener(listener: DataListener<T>): Subscription {
listener.onDataChanged(data)
return subscription
}
}

View File

@@ -1,179 +0,0 @@
/*
* 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.statusbar.notification.people
import android.graphics.drawable.Drawable
import android.testing.AndroidTestingRunner
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
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)
class PeopleHubViewControllerTest : SysuiTestCase() {
@JvmField @Rule val mockito: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var mockViewBoundary: PeopleHubViewBoundary
@Mock private lateinit var mockActivityStarter: ActivityStarter
@Test
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<PersonViewModel?>()
val fakePersonViewAdapter2 = FakeDataListener<PersonViewModel?>()
whenever(mockViewBoundary.personViewAdapters)
.thenReturn(sequenceOf(fakePersonViewAdapter1, fakePersonViewAdapter2))
val adapter = PeopleHubViewAdapterImpl(FakeDataSource(mockFactory))
adapter.bindView(mockViewBoundary)
assertThat(fakePersonViewAdapter1.lastSeen).isEqualTo(Maybe.Just(fakePerson1))
assertThat(fakePersonViewAdapter2.lastSeen).isEqualTo(Maybe.Just<PersonViewModel?>(null))
verify(mockViewBoundary).setVisible(true)
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<PersonViewModel?>()
val fakePersonViewAdapter2 = FakeDataListener<PersonViewModel?>()
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 fakeClickRunnable = mock(Runnable::class.java)
val fakePerson = fakePersonModel("id", "name", fakeClickRunnable)
val fakeModel = PeopleHubModel(listOf(fakePerson))
val fakeModelDataSource = FakeDataSource(fakeModel)
val factoryDataSource = PeopleHubViewModelFactoryDataSourceImpl(
mockActivityStarter,
fakeModelDataSource
)
val fakeListener = FakeDataListener<PeopleHubViewModelFactory>()
val mockClickView = mock(View::class.java)
factoryDataSource.registerListener(fakeListener)
val viewModel = (fakeListener.lastSeen as Maybe.Just).value
.createWithAssociatedClickView(mockClickView)
assertThat(viewModel.isVisible).isTrue()
val people = viewModel.people.toList()
assertThat(people.size).isEqualTo(1)
assertThat(people[0].name).isEqualTo("name")
assertThat(people[0].icon).isSameInstanceAs(fakePerson.avatar)
people[0].onClick()
verify(fakeClickRunnable).run()
}
}
/** Works around Mockito matchers returning `null` and breaking non-nullable Kotlin code. */
private inline fun <reified T : Any> any(): T {
return Mockito.any() ?: createInstance(T::class)
}
/** Works around Mockito matchers returning `null` and breaking non-nullable Kotlin code. */
private inline fun <reified T : Any> same(value: T): T {
return Mockito.same(value) ?: createInstance(T::class)
}
/** Creates an instance of the given class. */
private fun <T : Any> createInstance(clazz: KClass<T>): T = castNull()
/** Tricks the Kotlin compiler into assigning `null` to a non-nullable variable. */
@Suppress("UNCHECKED_CAST")
private fun <T> castNull(): T = null as T
private fun fakePersonModel(
id: String,
name: CharSequence,
clickRunnable: Runnable,
userId: Int = 0
): PersonModel =
PersonModel(id, userId, name, mock(Drawable::class.java), clickRunnable)
private fun fakePersonViewModel(name: CharSequence): PersonViewModel =
PersonViewModel(name, mock(Drawable::class.java), mock({}.javaClass))
sealed class Maybe<T> {
data class Just<T>(val value: T) : Maybe<T>()
class Nothing<T> : Maybe<T>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
}
class FakeDataListener<T> : DataListener<T> {
var lastSeen: Maybe<T> = Maybe.Nothing()
override fun onDataChanged(data: T) {
lastSeen = Maybe.Just(data)
}
}