Merge "Delete unused PeopleHub code" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
1035f8a36f
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user