diff options
15 files changed, 901 insertions, 14 deletions
diff --git a/packages/SystemUI/res/layout/people_strip.xml b/packages/SystemUI/res/layout/people_strip.xml new file mode 100644 index 000000000000..b314db8d72fd --- /dev/null +++ b/packages/SystemUI/res/layout/people_strip.xml @@ -0,0 +1,240 @@ +<?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="105dp"> + + <com.android.systemui.statusbar.notification.row.NotificationBackgroundView + android:id="@+id/backgroundNormal" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <com.android.systemui.statusbar.notification.row.NotificationBackgroundView + android:id="@+id/backgroundDimmed" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <LinearLayout + android:id="@+id/people_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:gravity="center" + android:orientation="horizontal"> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <LinearLayout + android:layout_width="70dp" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:visibility="invisible"> + + <ImageView + android:id="@+id/person_icon" + android:layout_width="36dp" + android:layout_height="36dp" + android:scaleType="fitCenter" + /> + + <TextView + android:id="@+id/person_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:ellipsize="end" + android:maxLines="2" + android:textAlignment="center" + /> + + </LinearLayout> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <LinearLayout + android:layout_width="70dp" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:visibility="invisible"> + + <ImageView + android:id="@+id/person_icon" + android:layout_width="36dp" + android:layout_height="36dp" + android:scaleType="fitCenter" + /> + + <TextView + android:id="@+id/person_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:ellipsize="end" + android:maxLines="2" + android:textAlignment="center" + /> + + </LinearLayout> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <LinearLayout + android:layout_width="70dp" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:visibility="invisible"> + + <ImageView + android:id="@+id/person_icon" + android:layout_width="36dp" + android:layout_height="36dp" + android:scaleType="fitCenter" + /> + + <TextView + android:id="@+id/person_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:ellipsize="end" + android:maxLines="2" + android:textAlignment="center" + /> + + </LinearLayout> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <LinearLayout + android:layout_width="70dp" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:visibility="invisible"> + + <ImageView + android:id="@+id/person_icon" + android:layout_width="36dp" + android:layout_height="36dp" + android:scaleType="fitCenter" + /> + + <TextView + android:id="@+id/person_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:ellipsize="end" + android:maxLines="2" + android:textAlignment="center" + /> + + </LinearLayout> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + <LinearLayout + android:layout_width="70dp" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:visibility="invisible"> + + <ImageView + android:id="@+id/person_icon" + android:layout_width="36dp" + android:layout_height="36dp" + android:scaleType="fitCenter" + /> + + <TextView + android:id="@+id/person_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:ellipsize="end" + android:maxLines="2" + android:textAlignment="center" + /> + + </LinearLayout> + + <View + android:layout_width="8dp" + android:layout_height="match_parent" + android:layout_weight="1" + /> + + </LinearLayout> + + <com.android.systemui.statusbar.notification.FakeShadowView + android:id="@+id/fake_shadow" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + +</com.android.systemui.statusbar.notification.stack.PeopleHubView>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/SystemUIModule.java index b0316e22de06..4520a1a6a037 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIModule.java @@ -23,6 +23,7 @@ import android.content.pm.PackageManager; 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 @@ import dagger.Provides; * 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 f56586802a68..6e464f480218 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 @@ public class NotificationEntryManager implements } @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 480cb78efbba..009551168010 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 @@ class NotificationSectionsFeatureManager @Inject constructor( 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 623ccca249a5..7e398bb70433 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 @@ public class NotificationData { 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 @@ public class NotificationData { 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 @@ public class NotificationData { 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 @@ public class NotificationData { } } + 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 000000000000..2c0c9420a8c4 --- /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 000000000000..8c067b79482c --- /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 000000000000..90a860a35a3e --- /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 000000000000..8d1253b457cc --- /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 000000000000..33e3bb883d53 --- /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 6ed4a576f441..bd87d7774e2c 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 static java.lang.annotation.RetentionPolicy.SOURCE; 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.R; 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.lang.annotation.Retention; 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 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section 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 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section 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 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section * 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 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section } if (!begin) { - begin = view == mGentleHeader; + begin = view == mGentleHeader || previous == mPeopleHubView; } return begin; @@ -161,6 +218,8 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section 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 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section return; } + int lastPersonIndex = -1; int firstGentleNotifIndex = -1; final int n = mParent.getChildCount(); @@ -183,6 +243,9 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section 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 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section } } + if (adjustPeopleHubVisibilityAndPosition(lastPersonIndex)) { + // make room for peopleHub + firstGentleNotifIndex++; + } + adjustGentleHeaderVisibilityAndPosition(firstGentleNotifIndex); mGentleHeader.setAreThereDismissableGentleNotifs( @@ -232,6 +300,36 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section } } + 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 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section } } + 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 5a1a2176672e..6dca7ee9e872 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.ShadeViewRefactor.RefactorCom 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 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd FalsingManager falsingManager, NotificationLockscreenUserManager notificationLockscreenUserManager, NotificationGutsManager notificationGutsManager, - NotificationSectionsFeatureManager sectionsFeatureManager) { + NotificationSectionsFeatureManager sectionsFeatureManager, + PeopleHubSectionFooterViewAdapter peopleHubViewAdapter) { super(context, attrs, 0, 0); Resources res = getResources(); @@ -541,6 +543,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd 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 000000000000..e31ee024fa36 --- /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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 56ed0e3a6af3..003d80376c40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -44,6 +44,7 @@ import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.notification.people.PeopleHubSectionFooterViewAdapter; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -66,6 +67,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Mock private ActivityStarterDelegate mActivityStarterDelegate; @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; + @Mock private PeopleHubSectionFooterViewAdapter mPeopleHubAdapter; private NotificationSectionsManager mSectionsManager; @@ -77,6 +79,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mActivityStarterDelegate, mStatusBarStateController, mConfigurationController, + mPeopleHubAdapter, 2); // Required in order for the header inflation to work properly when(mNssl.generateLayoutParams(any(AttributeSet.class))) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 4b82f59e2a35..012ebf728c50 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -68,6 +68,7 @@ import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.people.PeopleHubSectionFooterViewAdapter; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.FooterView; import com.android.systemui.statusbar.notification.row.NotificationBlockingHelperManager; @@ -171,7 +172,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { new FalsingManagerFake(), mock(NotificationLockscreenUserManager.class), mock(NotificationGutsManager.class), - new NotificationSectionsFeatureManager(new DeviceConfigProxyFake(), mContext)); + new NotificationSectionsFeatureManager(new DeviceConfigProxyFake(), mContext), + mock(PeopleHubSectionFooterViewAdapter.class)); mStackScroller = spy(mStackScrollerInternal); mStackScroller.setShelf(notificationShelf); mStackScroller.setStatusBar(mBar); |