Add PeopleHub to the notification shade

When the people section of the shade is enabled, an additional entry
will appear at the bottom of the section that contains shortcuts to
recent conversations that have appeared as notifications.

Test: manual
Change-Id: Iac94cabe3dd4233510ddc50b3a1923ed42c552e0
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/SystemUIModule.java
index b0316e22..4520a1a6 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIModule.java
@@ -23,6 +23,7 @@
 import com.android.systemui.assist.AssistModule;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.notification.people.PeopleHubModule;
 import com.android.systemui.statusbar.phone.KeyguardLiftController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
 
@@ -35,7 +36,7 @@
  * A dagger module for injecting components of System UI that are not overridden by the System UI
  * implementation.
  */
-@Module(includes = {AssistModule.class, ComponentBinder.class})
+@Module(includes = {AssistModule.class, ComponentBinder.class, PeopleHubModule.class})
 public abstract class SystemUIModule {
 
     @Singleton
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index f565868..6e464f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -126,7 +126,9 @@
     }
 
     @Inject
-    public NotificationEntryManager(NotificationData notificationData, NotifLog notifLog) {
+    public NotificationEntryManager(
+            NotificationData notificationData,
+            NotifLog notifLog) {
         mNotificationData = notificationData;
         mNotifLog = notifLog;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index 480cb78..0095511 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -66,7 +66,7 @@
 private fun usePeopleFiltering(proxy: DeviceConfigProxy): Boolean {
     if (sUsePeopleFiltering == null) {
         sUsePeopleFiltering = proxy.getBoolean(
-                DeviceConfig.NAMESPACE_SYSTEMUI, NOTIFICATIONS_USE_PEOPLE_FILTERING, false)
+                DeviceConfig.NAMESPACE_SYSTEMUI, NOTIFICATIONS_USE_PEOPLE_FILTERING, true)
     }
 
     return sUsePeopleFiltering!!
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
index 623ccca..7e398bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
@@ -98,6 +98,9 @@
             int aRank = getRank(a.getKey());
             int bRank = getRank(b.getKey());
 
+            boolean aPeople = isPeopleNotification(a);
+            boolean bPeople = isPeopleNotification(b);
+
             boolean aMedia = isImportantMedia(a);
             boolean bMedia = isImportantMedia(b);
 
@@ -107,8 +110,8 @@
             boolean aHeadsUp = a.isRowHeadsUp();
             boolean bHeadsUp = b.isRowHeadsUp();
 
-            if (mUsePeopleFiltering && a.hasAssociatedPeople() != b.hasAssociatedPeople()) {
-                return a.hasAssociatedPeople() ? -1 : 1;
+            if (mUsePeopleFiltering && aPeople != bPeople) {
+                return aPeople ? -1 : 1;
             } else if (aHeadsUp != bHeadsUp) {
                 return aHeadsUp ? -1 : 1;
             } else if (aHeadsUp) {
@@ -447,7 +450,7 @@
             boolean isHeadsUp,
             boolean isMedia,
             boolean isSystemMax) {
-        if (mUsePeopleFiltering && e.hasAssociatedPeople()) {
+        if (mUsePeopleFiltering && isPeopleNotification(e)) {
             e.setBucket(BUCKET_PEOPLE);
         } else if (isHeadsUp || isMedia || isSystemMax || e.isHighPriority()) {
             e.setBucket(BUCKET_ALERTING);
@@ -456,6 +459,11 @@
         }
     }
 
+    private boolean isPeopleNotification(NotificationEntry e) {
+        return e.getSbn().getNotification().getNotificationStyle()
+                == Notification.MessagingStyle.class;
+    }
+
     public void dump(PrintWriter pw, String indent) {
         int filteredLen = mSortedAndFiltered.size();
         pw.print(indent);
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
new file mode 100644
index 0000000..2c0c942
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHub.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.people
+
+import android.app.PendingIntent
+import android.graphics.drawable.Drawable
+
+/** `ViewModel` for PeopleHub view. */
+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. */
+data class PeopleHubModel(val people: Collection<PersonModel>)
+
+/** `Model` for a single "Person" in PeopleHub. */
+data class PersonModel(
+    val key: PersonKey,
+    val name: CharSequence,
+    val avatar: Drawable,
+    val clickIntent: PendingIntent
+)
+
+/** Unique identifier for a Person in PeopleHub. */
+typealias PersonKey = String
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubModule.kt
new file mode 100644
index 0000000..8c067b7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubModule.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 dagger.Binds
+import dagger.Module
+
+@Module
+abstract class PeopleHubModule {
+
+    @Binds
+    abstract fun peopleHubSectionFooterViewController(
+        viewAdapter: PeopleHubSectionFooterViewAdapterImpl
+    ): PeopleHubSectionFooterViewAdapter
+
+    @Binds
+    abstract fun peopleHubDataSource(s: PeopleHubDataSourceImpl): DataSource<PeopleHubModel>
+
+    @Binds
+    abstract fun peopleHubViewModelFactoryDataSource(
+        dataSource: PeopleHubViewModelFactoryDataSourceImpl
+    ): DataSource<PeopleHubViewModelFactory>
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..90a860a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt
@@ -0,0 +1,211 @@
+/*
+ * 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.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.PixelFormat
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.util.TypedValue
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import com.android.internal.statusbar.NotificationVisibility
+import com.android.internal.widget.MessagingGroup
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.systemui.R
+import com.android.systemui.statusbar.notification.NotificationEntryListener
+import com.android.systemui.statusbar.notification.NotificationEntryManager
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.util.ArrayDeque
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val MAX_STORED_INACTIVE_PEOPLE = 10
+
+@Singleton
+class PeopleHubDataSourceImpl @Inject constructor(
+    notificationEntryManager: NotificationEntryManager,
+    private val peopleHubManager: PeopleHubManager
+) : DataSource<PeopleHubModel> {
+
+    private var dataListener: DataListener<PeopleHubModel>? = null
+
+    init {
+        notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener {
+            override fun onEntryInflated(entry: NotificationEntry, inflatedFlags: Int) =
+                    addVisibleEntry(entry)
+
+            override fun onEntryReinflated(entry: NotificationEntry) = addVisibleEntry(entry)
+
+            override fun onPostEntryUpdated(entry: NotificationEntry) = addVisibleEntry(entry)
+
+            override fun onEntryRemoved(
+                entry: NotificationEntry,
+                visibility: NotificationVisibility?,
+                removedByUser: Boolean
+            ) = removeVisibleEntry(entry)
+        })
+    }
+
+    private fun removeVisibleEntry(entry: NotificationEntry?) {
+        if (entry?.extractPersonKey()?.let(peopleHubManager::removeActivePerson) == true) {
+            updateUi()
+        }
+    }
+
+    private fun addVisibleEntry(entry: NotificationEntry?) {
+        if (entry?.extractPerson()?.let(peopleHubManager::addActivePerson) == true) {
+            updateUi()
+        }
+    }
+
+    override fun setListener(listener: DataListener<PeopleHubModel>) {
+        this.dataListener = listener
+        updateUi()
+    }
+
+    private fun updateUi() {
+        dataListener?.onDataChanged(peopleHubManager.getPeopleHubModel())
+    }
+}
+
+@Singleton
+class PeopleHubManager @Inject constructor() {
+
+    private val activePeople = mutableMapOf<PersonKey, PersonModel>()
+    private val inactivePeople = ArrayDeque<PersonModel>(MAX_STORED_INACTIVE_PEOPLE)
+
+    fun removeActivePerson(key: PersonKey): Boolean {
+        activePeople.remove(key)?.let { data ->
+            if (inactivePeople.size >= MAX_STORED_INACTIVE_PEOPLE) {
+                inactivePeople.removeLast()
+            }
+            inactivePeople.push(data)
+            return true
+        }
+        return false
+    }
+
+    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 }
+
+private fun NotificationEntry.extractPerson(): PersonModel? {
+    if (!isMessagingNotification()) {
+        return null
+    }
+
+    val clickIntent = sbn.notification.contentIntent
+    val extras = sbn.notification.extras
+    val name = extras.getString(Notification.EXTRA_CONVERSATION_TITLE)
+            ?: extras.getString(Notification.EXTRA_TITLE)
+            ?: return null
+    val drawable = extractAvatarFromRow(this) ?: return null
+
+    val context = row.context
+    val pm = context.packageManager
+    val appInfo = pm.getApplicationInfo(sbn.packageName, 0)
+
+    val badgedAvatar = object : Drawable() {
+        override fun draw(canvas: Canvas) {
+            val iconBounds = getBounds()
+            val factory = object : BaseIconFactory(
+                    context,
+                    0 /* unused */,
+                    iconBounds.width(),
+                    true) {}
+            val badge = factory.createBadgedIconBitmap(
+                    appInfo.loadIcon(pm),
+                    sbn.user,
+                    true,
+                    appInfo.isInstantApp,
+                    null)
+            val badgeDrawable = BitmapDrawable(context.resources, badge.icon)
+                    .apply {
+                        alpha = drawable.alpha
+                        colorFilter = drawable.colorFilter
+                        val badgeWidth = TypedValue.applyDimension(
+                                TypedValue.COMPLEX_UNIT_DIP,
+                                16f,
+                                context.resources.displayMetrics
+                        ).toInt()
+                        setBounds(
+                                iconBounds.left + (iconBounds.width() - badgeWidth),
+                                iconBounds.top + (iconBounds.height() - badgeWidth),
+                                iconBounds.right,
+                                iconBounds.bottom)
+                    }
+            drawable.bounds = iconBounds
+            drawable.draw(canvas)
+            badgeDrawable.draw(canvas)
+        }
+
+        override fun setAlpha(alpha: Int) {
+            drawable.alpha = alpha
+        }
+
+        override fun setColorFilter(colorFilter: ColorFilter?) {
+            drawable.colorFilter = colorFilter
+        }
+
+        @PixelFormat.Opacity
+        override fun getOpacity(): Int = PixelFormat.OPAQUE
+    }
+
+    return PersonModel(key, name, badgedAvatar, clickIntent)
+}
+
+private 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()
+
+private fun NotificationEntry.extractPersonKey(): PersonKey? =
+        if (isMessagingNotification()) key else null
+
+private fun NotificationEntry.isMessagingNotification() =
+        sbn.notification.notificationStyle == Notification.MessagingStyle::class.java
\ No newline at end of file
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
new file mode 100644
index 0000000..8d1253b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.view.View
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Boundary between the View and PeopleHub, as seen by the View. */
+interface PeopleHubSectionFooterViewAdapter {
+    fun bindView(viewBoundary: PeopleHubSectionFooterViewBoundary)
+}
+
+/** Abstract `View` representation of PeopleHub footer in [NotificationSectionsManager]. */
+interface PeopleHubSectionFooterViewBoundary {
+    /** 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. */
+    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 [PeopleHubSectionFooterViewBoundary] in a [DataListener], and connects it to the data
+ * pipeline.
+ *
+ * @param dataSource PeopleHub data pipeline.
+ */
+@Singleton
+class PeopleHubSectionFooterViewAdapterImpl @Inject constructor(
+    private val dataSource: DataSource<@JvmSuppressWildcards PeopleHubViewModelFactory>
+) : PeopleHubSectionFooterViewAdapter {
+
+    override fun bindView(viewBoundary: PeopleHubSectionFooterViewBoundary) =
+            dataSource.setListener(PeopleHubDataListenerImpl(viewBoundary))
+}
+
+private class PeopleHubDataListenerImpl(
+    private val viewBoundary: PeopleHubSectionFooterViewBoundary
+) : DataListener<PeopleHubViewModelFactory> {
+
+    override fun onDataChanged(data: PeopleHubViewModelFactory) {
+        val viewModel = data.createWithAssociatedClickView(
+                viewBoundary.associatedViewForClickAnimation
+        )
+        viewBoundary.setVisible(viewModel.isVisible)
+        val padded = viewModel.people + repeated(null)
+        for ((personAdapter, personModel) in viewBoundary.personViewAdapters.zip(padded)) {
+            personAdapter.onDataChanged(personModel)
+        }
+    }
+}
+
+/**
+ * Converts [PeopleHubModel]s into [PeopleHubViewModelFactory]s.
+ *
+ * This class serves as the glue between the View layer (which depends on
+ * [PeopleHubSectionFooterViewBoundary]) and the Data layer (which produces [PeopleHubModel]s).
+ */
+@Singleton
+class PeopleHubViewModelFactoryDataSourceImpl @Inject constructor(
+    private val activityStarter: ActivityStarter,
+    private val dataSource: DataSource<@JvmSuppressWildcards PeopleHubModel>
+) : DataSource<PeopleHubViewModelFactory> {
+
+    override fun setListener(listener: DataListener<PeopleHubViewModelFactory>) =
+            dataSource.setListener(PeopleHubModelListenerImpl(activityStarter, listener))
+}
+
+private class PeopleHubModelListenerImpl(
+    private val activityStarter: ActivityStarter,
+    private val dataListener: DataListener<PeopleHubViewModelFactory>
+) : DataListener<PeopleHubModel> {
+
+    override fun onDataChanged(data: PeopleHubModel) =
+            dataListener.onDataChanged(PeopleHubViewModelFactoryImpl(data, activityStarter))
+}
+
+private class PeopleHubViewModelFactoryImpl(
+    private val data: PeopleHubModel,
+    private val activityStarter: ActivityStarter
+) : PeopleHubViewModelFactory {
+
+    override fun createWithAssociatedClickView(view: View): PeopleHubViewModel {
+        val personViewModels = data.people.asSequence().map { personModel ->
+            val onClick = {
+                activityStarter.startPendingIntentDismissingKeyguard(
+                        personModel.clickIntent,
+                        null,
+                        view
+                )
+            }
+            PersonViewModel(personModel.name, personModel.avatar, onClick)
+        }
+        return PeopleHubViewModel(personViewModels, data.people.isNotEmpty())
+    }
+}
+
+private fun <T> repeated(value: T): Sequence<T> = sequence {
+    while (true) {
+        yield(value)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/ViewPipeline.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/ViewPipeline.kt
new file mode 100644
index 0000000..33e3bb8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/ViewPipeline.kt
@@ -0,0 +1,37 @@
+/*
+ * 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
+
+/** Boundary between a View and data pipeline, as seen by the pipeline. */
+interface DataListener<in T> {
+    fun onDataChanged(data: T)
+}
+
+/** Convert all data using the given [mapper] before invoking this [DataListener]. */
+fun <S, T> DataListener<T>.contraMap(mapper: (S) -> T): DataListener<S> = object : DataListener<S> {
+    override fun onDataChanged(data: S) = onDataChanged(mapper(data))
+}
+
+/** Boundary between a View and data pipeline, as seen by the View. */
+interface DataSource<out T> {
+    fun setListener(listener: DataListener<T>)
+}
+
+/** Transform all data coming out of this [DataSource] using the given [mapper]. */
+fun <S, T> DataSource<S>.map(mapper: (S) -> T): DataSource<T> = object : DataSource<T> {
+    override fun setListener(listener: DataListener<T>) = setListener(listener.contraMap(mapper))
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
index 6ed4a57..bd87d77 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
@@ -23,6 +23,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.PendingIntent;
 import android.content.Intent;
 import android.provider.Settings;
 import android.view.LayoutInflater;
@@ -33,6 +34,10 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.people.DataListener;
+import com.android.systemui.statusbar.notification.people.PeopleHubSectionFooterViewAdapter;
+import com.android.systemui.statusbar.notification.people.PeopleHubSectionFooterViewBoundary;
+import com.android.systemui.statusbar.notification.people.PersonViewModel;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -42,6 +47,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import kotlin.sequences.Sequence;
+
 /**
  * Manages the boundaries of the two notification sections (high priority and low priority). Also
  * shows/hides the headers for those sections where appropriate.
@@ -58,11 +65,39 @@
     private final StatusBarStateController mStatusBarStateController;
     private final ConfigurationController mConfigurationController;
     private final int mNumberOfSections;
-
     private boolean mInitialized = false;
+
     private SectionHeaderView mGentleHeader;
     private boolean mGentleHeaderVisible = false;
 
+    private boolean mPeopleHubVisible = false;
+    private PeopleHubView mPeopleHubView;
+    private final PeopleHubSectionFooterViewAdapter mPeopleHubViewAdapter;
+    private final PeopleHubSectionFooterViewBoundary mPeopleHubViewBoundary =
+            new PeopleHubSectionFooterViewBoundary() {
+                @Override
+                public void setVisible(boolean isVisible) {
+                    if (mPeopleHubVisible != isVisible) {
+                        mPeopleHubVisible = isVisible;
+                        if (mInitialized) {
+                            updateSectionBoundaries();
+                        }
+                    }
+                }
+
+                @NonNull
+                @Override
+                public View getAssociatedViewForClickAnimation() {
+                    return mPeopleHubView;
+                }
+
+                @NonNull
+                @Override
+                public Sequence<DataListener<PersonViewModel>> getPersonViewAdapters() {
+                    return mPeopleHubView.getPersonViewAdapters();
+                }
+            };
+
     @Nullable private View.OnClickListener mOnClearGentleNotifsClickListener;
 
     NotificationSectionsManager(
@@ -70,11 +105,13 @@
             ActivityStarter activityStarter,
             StatusBarStateController statusBarStateController,
             ConfigurationController configurationController,
+            PeopleHubSectionFooterViewAdapter peopleHubViewAdapter,
             int numberOfSections) {
         mParent = parent;
         mActivityStarter = activityStarter;
         mStatusBarStateController = statusBarStateController;
         mConfigurationController = configurationController;
+        mPeopleHubViewAdapter = peopleHubViewAdapter;
         mNumberOfSections = numberOfSections;
     }
 
@@ -101,23 +138,43 @@
      * Reinflates the entire notification header, including all decoration views.
      */
     void reinflateViews(LayoutInflater layoutInflater) {
-        int oldPos = -1;
+        int oldGentleHeaderPos = -1;
+        int oldPeopleHubPos = -1;
         if (mGentleHeader != null) {
             if (mGentleHeader.getTransientContainer() != null) {
                 mGentleHeader.getTransientContainer().removeView(mGentleHeader);
             } else if (mGentleHeader.getParent() != null) {
-                oldPos = mParent.indexOfChild(mGentleHeader);
+                oldGentleHeaderPos = mParent.indexOfChild(mGentleHeader);
                 mParent.removeView(mGentleHeader);
             }
         }
+        if (mPeopleHubView != null) {
+            if (mPeopleHubView.getTransientContainer() != null) {
+                mPeopleHubView.getTransientContainer().removeView(mPeopleHubView);
+            } else if (mPeopleHubView.getParent() != null) {
+                oldPeopleHubPos = mParent.indexOfChild(mPeopleHubView);
+                mParent.removeView(mPeopleHubView);
+            }
+        }
 
         mGentleHeader = (SectionHeaderView) layoutInflater.inflate(
                 R.layout.status_bar_notification_section_header, mParent, false);
         mGentleHeader.setOnHeaderClickListener(this::onGentleHeaderClick);
         mGentleHeader.setOnClearAllClickListener(this::onClearGentleNotifsClick);
 
-        if (oldPos != -1) {
-            mParent.addView(mGentleHeader, oldPos);
+        if (oldGentleHeaderPos != -1) {
+            mParent.addView(mGentleHeader, oldGentleHeaderPos);
+        }
+
+        mPeopleHubView = (PeopleHubView) layoutInflater.inflate(
+                R.layout.people_strip, mParent, false);
+
+        if (oldPeopleHubPos != -1) {
+            mParent.addView(mPeopleHubView, oldPeopleHubPos);
+        }
+
+        if (!mInitialized) {
+            mPeopleHubViewAdapter.bindView(mPeopleHubViewBoundary);
         }
     }
 
@@ -145,7 +202,7 @@
         }
 
         if (!begin) {
-            begin = view == mGentleHeader;
+            begin = view == mGentleHeader || previous == mPeopleHubView;
         }
 
         return begin;
@@ -161,6 +218,8 @@
             return ((ExpandableNotificationRow) view).getEntry().getBucket();
         } else if (view == mGentleHeader) {
             return BUCKET_SILENT;
+        } else if (view == mPeopleHubView) {
+            return BUCKET_PEOPLE;
         }
 
         throw new IllegalArgumentException("I don't know how to find a bucket for this view :(");
@@ -175,6 +234,7 @@
             return;
         }
 
+        int lastPersonIndex = -1;
         int firstGentleNotifIndex = -1;
 
         final int n = mParent.getChildCount();
@@ -183,6 +243,9 @@
             if (child instanceof ExpandableNotificationRow
                     && child.getVisibility() != View.GONE) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                if (row.getEntry().getBucket() == BUCKET_PEOPLE) {
+                    lastPersonIndex = i;
+                }
                 if (row.getEntry().getBucket() == BUCKET_SILENT) {
                     firstGentleNotifIndex = i;
                     break;
@@ -190,6 +253,11 @@
             }
         }
 
+        if (adjustPeopleHubVisibilityAndPosition(lastPersonIndex)) {
+            // make room for peopleHub
+            firstGentleNotifIndex++;
+        }
+
         adjustGentleHeaderVisibilityAndPosition(firstGentleNotifIndex);
 
         mGentleHeader.setAreThereDismissableGentleNotifs(
@@ -232,6 +300,36 @@
         }
     }
 
+    private boolean adjustPeopleHubVisibilityAndPosition(int lastPersonIndex) {
+        final boolean showPeopleHeader = mPeopleHubVisible
+                && mNumberOfSections > 2
+                && mStatusBarStateController.getState() != StatusBarState.KEYGUARD;
+        final int currentHubIndex = mParent.indexOfChild(mPeopleHubView);
+        final boolean currentlyVisible = currentHubIndex >= 0;
+        int targetIndex = lastPersonIndex + 1;
+
+        if (!showPeopleHeader) {
+            if (currentlyVisible) {
+                mParent.removeView(mPeopleHubView);
+            }
+        } else {
+            if (!currentlyVisible) {
+                if (mPeopleHubView.getTransientContainer() != null) {
+                    mPeopleHubView.getTransientContainer().removeTransientView(mPeopleHubView);
+                    mPeopleHubView.setTransientContainer(null);
+                }
+                mParent.addView(mPeopleHubView, targetIndex);
+                return true;
+            } else if (currentHubIndex != targetIndex - 1) {
+                if (currentHubIndex < targetIndex) {
+                    targetIndex--;
+                }
+                mParent.changeViewPosition(mPeopleHubView, targetIndex);
+            }
+        }
+        return false;
+    }
+
     /**
      * Updates the boundaries (as tracked by their first and last views) of the priority sections.
      *
@@ -324,6 +422,10 @@
         }
     }
 
+    private void handlePeopleHubClick(PendingIntent pendingIntent) {
+        mActivityStarter.startPendingIntentDismissingKeyguard(pendingIntent, null, mPeopleHubView);
+    }
+
     /**
      * For now, declare the available notification buckets (sections) here so that other
      * presentation code can decide what to do based on an entry's buckets
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 5a1a217..6dca7ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -116,6 +116,7 @@
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
+import com.android.systemui.statusbar.notification.people.PeopleHubSectionFooterViewAdapter;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -518,7 +519,8 @@
             FalsingManager falsingManager,
             NotificationLockscreenUserManager notificationLockscreenUserManager,
             NotificationGutsManager notificationGutsManager,
-            NotificationSectionsFeatureManager sectionsFeatureManager) {
+            NotificationSectionsFeatureManager sectionsFeatureManager,
+            PeopleHubSectionFooterViewAdapter peopleHubViewAdapter) {
         super(context, attrs, 0, 0);
         Resources res = getResources();
 
@@ -541,6 +543,7 @@
                         activityStarter,
                         statusBarStateController,
                         configurationController,
+                        peopleHubViewAdapter,
                         buckets.length);
         mSectionsManager.initialize(LayoutInflater.from(context));
         mSectionsManager.setOnClearGentleNotifsClickListener(v -> {
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
new file mode 100644
index 0000000..e31ee02
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.statusbar.notification.people.PersonViewModel
+import com.android.systemui.statusbar.notification.people.DataListener
+import com.android.systemui.statusbar.notification.row.ActivatableNotificationView
+
+class PeopleHubView(context: Context, attrs: AttributeSet) :
+        ActivatableNotificationView(context, attrs) {
+
+    private lateinit var contents: ViewGroup
+    private lateinit var personControllers: List<PersonDataListenerImpl>
+    val personViewAdapters: Sequence<DataListener<PersonViewModel?>>
+        get() = personControllers.asSequence()
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        contents = requireViewById(R.id.people_list)
+        personControllers = (0 until contents.childCount)
+                .asSequence()
+                .mapNotNull { idx ->
+                    (contents.getChildAt(idx) as? LinearLayout)?.let(::PersonDataListenerImpl)
+                }
+                .toList()
+    }
+
+    override fun getContentView(): View = contents
+
+    private inner class PersonDataListenerImpl(val viewGroup: ViewGroup) :
+            DataListener<PersonViewModel?> {
+
+        val nameView = viewGroup.requireViewById<TextView>(R.id.person_name)
+        val avatarView = viewGroup.requireViewById<ImageView>(R.id.person_icon)
+
+        override fun onDataChanged(data: PersonViewModel?) {
+            viewGroup.visibility = data?.let { View.VISIBLE } ?: View.INVISIBLE
+            nameView.text = data?.name
+            avatarView.setImageDrawable(data?.icon)
+            viewGroup.setOnClickListener { data?.onClick?.invoke() }
+        }
+    }
+}
\ No newline at end of file