Plugin interface for PeopleHub

This allows for plugins to add additional "people notification"
detection logic, useful for prototyping against apps which do
not post MessagingStyle notifications.

Test: manual
Change-Id: I8b7b824beed50dfb86689f41ff151b3014737ffb
This commit is contained in:
Steve Elliott
2019-10-21 15:28:05 -04:00
parent 26d4e011de
commit 984cfec745
16 changed files with 277 additions and 67 deletions

View File

@@ -0,0 +1,72 @@
/*
* 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.plugins;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.graphics.drawable.Drawable;
import android.service.notification.StatusBarNotification;
import com.android.systemui.plugins.annotations.DependsOn;
import com.android.systemui.plugins.annotations.ProvidesInterface;
/** Custom logic that can extract a PeopleHub "person" from a notification. */
@ProvidesInterface(
action = NotificationPersonExtractorPlugin.ACTION,
version = NotificationPersonExtractorPlugin.VERSION)
@DependsOn(target = NotificationPersonExtractorPlugin.PersonData.class)
public interface NotificationPersonExtractorPlugin extends Plugin {
String ACTION = "com.android.systemui.action.PEOPLE_HUB_PERSON_EXTRACTOR";
int VERSION = 0;
/**
* Attempts to extract a person from a notification. Returns {@code null} if one is not found.
*/
@Nullable PersonData extractPerson(StatusBarNotification sbn);
/**
* Attempts to extract a person id from a notification. Returns {@code null} if one is not
* found.
*
* This method can be overridden in order to provide a faster implementation.
*/
@Nullable
default String extractPersonKey(StatusBarNotification sbn) {
return extractPerson(sbn).key;
}
/** A person to be surfaced in PeopleHub. */
@ProvidesInterface(version = PersonData.VERSION)
final class PersonData {
public static final int VERSION = 0;
public final String key;
public final CharSequence name;
public final Drawable avatar;
public final PendingIntent clickIntent;
public PersonData(String key, CharSequence name, Drawable avatar,
PendingIntent clickIntent) {
this.key = key;
this.name = name;
this.avatar = avatar;
this.clickIntent = clickIntent;
}
}
}

View File

@@ -33,10 +33,9 @@
<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:layout_height="match_parent"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:gravity="center"
android:orientation="horizontal">
@@ -49,7 +48,7 @@
<LinearLayout
android:layout_width="70dp"
android:layout_height="match_parent"
android:gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical"
android:visibility="invisible">
@@ -87,7 +86,7 @@
<LinearLayout
android:layout_width="70dp"
android:layout_height="match_parent"
android:gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical"
android:visibility="invisible">
@@ -125,7 +124,7 @@
<LinearLayout
android:layout_width="70dp"
android:layout_height="match_parent"
android:gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical"
android:visibility="invisible">
@@ -163,7 +162,7 @@
<LinearLayout
android:layout_width="70dp"
android:layout_height="match_parent"
android:gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical"
android:visibility="invisible">
@@ -201,7 +200,7 @@
<LinearLayout
android:layout_width="70dp"
android:layout_height="match_parent"
android:gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical"
android:visibility="invisible">

View File

@@ -138,6 +138,14 @@ public class NotificationEntryManager implements
mNotificationEntryListeners.add(listener);
}
/**
* Removes a {@link NotificationEntryListener} previously registered via
* {@link #addNotificationEntryListener(NotificationEntryListener)}.
*/
public void removeNotificationEntryListener(NotificationEntryListener listener) {
mNotificationEntryListeners.remove(listener);
}
/** Sets the {@link NotificationRemoveInterceptor}. */
public void setNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) {
mRemoveInterceptor = interceptor;

View File

@@ -37,6 +37,7 @@ import com.android.systemui.statusbar.notification.NotificationFilter;
import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
import com.android.systemui.statusbar.notification.logging.NotifEvent;
import com.android.systemui.statusbar.notification.logging.NotifLog;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -76,12 +77,16 @@ public class NotificationData {
private final Ranking mTmpRanking = new Ranking();
private final boolean mUsePeopleFiltering;
private final NotifLog mNotifLog;
private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
@Inject
public NotificationData(NotificationSectionsFeatureManager sectionsFeatureManager,
NotifLog notifLog) {
public NotificationData(
NotificationSectionsFeatureManager sectionsFeatureManager,
NotifLog notifLog,
PeopleNotificationIdentifier peopleNotificationIdentifier) {
mUsePeopleFiltering = sectionsFeatureManager.isFilteringEnabled();
mNotifLog = notifLog;
mPeopleNotificationIdentifier = peopleNotificationIdentifier;
}
public void setHeadsUpManager(HeadsUpManager headsUpManager) {
@@ -460,8 +465,7 @@ public class NotificationData {
}
private boolean isPeopleNotification(NotificationEntry e) {
return e.getSbn().getNotification().getNotificationStyle()
== Notification.MessagingStyle.class;
return mPeopleNotificationIdentifier.isPeopleNotification(e.getSbn());
}
public void dump(PrintWriter pw, String indent) {

View File

@@ -24,14 +24,24 @@ abstract class PeopleHubModule {
@Binds
abstract fun peopleHubSectionFooterViewController(
viewAdapter: PeopleHubSectionFooterViewAdapterImpl
impl: PeopleHubSectionFooterViewAdapterImpl
): PeopleHubSectionFooterViewAdapter
@Binds
abstract fun peopleHubDataSource(s: PeopleHubDataSourceImpl): DataSource<PeopleHubModel>
abstract fun peopleHubDataSource(impl: PeopleHubDataSourceImpl): DataSource<PeopleHubModel>
@Binds
abstract fun peopleHubViewModelFactoryDataSource(
dataSource: PeopleHubViewModelFactoryDataSourceImpl
impl: PeopleHubViewModelFactoryDataSourceImpl
): DataSource<PeopleHubViewModelFactory>
@Binds
abstract fun peopleNotificationIdentifier(
impl: PeopleNotificationIdentifierImpl
): PeopleNotificationIdentifier
@Binds
abstract fun notificationPersonExtractor(
pluginImpl: NotificationPersonExtractorPluginBoundary
): NotificationPersonExtractor
}

View File

@@ -17,11 +17,14 @@
package com.android.systemui.statusbar.notification.people
import android.app.Notification
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.service.notification.StatusBarNotification
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
@@ -30,59 +33,112 @@ 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.plugins.NotificationPersonExtractorPlugin
import com.android.systemui.statusbar.notification.NotificationEntryListener
import com.android.systemui.statusbar.notification.NotificationEntryManager
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.policy.ExtensionController
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> {
interface NotificationPersonExtractor {
fun extractPerson(sbn: StatusBarNotification): PersonModel?
fun extractPersonKey(sbn: StatusBarNotification): String?
}
private var dataListener: DataListener<PeopleHubModel>? = null
@Singleton
class NotificationPersonExtractorPluginBoundary @Inject constructor(
extensionController: ExtensionController,
private val context: Context
) : NotificationPersonExtractor {
private var plugin: NotificationPersonExtractorPlugin? = null
init {
notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener {
override fun onEntryInflated(entry: NotificationEntry, inflatedFlags: Int) =
addVisibleEntry(entry)
plugin = extensionController
.newExtension(NotificationPersonExtractorPlugin::class.java)
.withPlugin(NotificationPersonExtractorPlugin::class.java)
.withCallback { extractor ->
plugin = extractor
}
.build()
.get()
}
override fun onEntryReinflated(entry: NotificationEntry) = addVisibleEntry(entry)
override fun extractPerson(sbn: StatusBarNotification) =
plugin?.extractPerson(sbn)?.let { data ->
val badged = addBadgeToDrawable(data.avatar, context, sbn.packageName, sbn.user)
PersonModel(data.key, data.name, badged, data.clickIntent)
}
override fun onPostEntryUpdated(entry: NotificationEntry) = addVisibleEntry(entry)
override fun extractPersonKey(sbn: StatusBarNotification) = plugin?.extractPersonKey(sbn)
}
override fun onEntryRemoved(
entry: NotificationEntry,
visibility: NotificationVisibility?,
removedByUser: Boolean
) = removeVisibleEntry(entry)
})
@Singleton
class PeopleHubDataSourceImpl @Inject constructor(
private val notificationEntryManager: NotificationEntryManager,
private val peopleHubManager: PeopleHubManager,
private val extractor: NotificationPersonExtractor
) : DataSource<PeopleHubModel> {
private val dataListeners = mutableListOf<DataListener<PeopleHubModel>>()
private val notificationEntryListener = 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) {
val key = extractor.extractPersonKey(entry.sbn) ?: entry.extractPersonKey()
if (key?.let(peopleHubManager::removeActivePerson) == true) {
updateUi()
}
}
private fun addVisibleEntry(entry: NotificationEntry) {
if (entry.extractPerson()?.let(peopleHubManager::addActivePerson) == true) {
val personModel = extractor.extractPerson(entry.sbn) ?: entry.extractPerson()
if (personModel?.let(peopleHubManager::addActivePerson) == true) {
updateUi()
}
}
override fun setListener(listener: DataListener<PeopleHubModel>) {
this.dataListener = listener
updateUi()
override fun registerListener(listener: DataListener<PeopleHubModel>): Subscription {
val registerWithNotificationEntryManager = dataListeners.isEmpty()
dataListeners.add(listener)
if (registerWithNotificationEntryManager) {
notificationEntryManager.addNotificationEntryListener(notificationEntryListener)
} else {
listener.onDataChanged(peopleHubManager.getPeopleHubModel())
}
return object : Subscription {
override fun unsubscribe() {
dataListeners.remove(listener)
if (dataListeners.isEmpty()) {
notificationEntryManager
.removeNotificationEntryListener(notificationEntryListener)
}
}
}
}
private fun updateUi() {
dataListener?.onDataChanged(peopleHubManager.getPeopleHubModel())
val model = peopleHubManager.getPeopleHubModel()
for (listener in dataListeners) {
listener.onDataChanged(model)
}
}
}
@@ -124,19 +180,25 @@ 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 badgedAvatar = addBadgeToDrawable(drawable, row.context, sbn.packageName, sbn.user)
return PersonModel(key, name, badgedAvatar, clickIntent)
}
val context = row.context
private fun addBadgeToDrawable(
drawable: Drawable,
context: Context,
packageName: String,
user: UserHandle
): Drawable {
val pm = context.packageManager
val appInfo = pm.getApplicationInfoAsUser(sbn.packageName, 0, sbn.user)
val badgedAvatar = object : Drawable() {
val appInfo = pm.getApplicationInfoAsUser(packageName, 0, user)
return object : Drawable() {
override fun draw(canvas: Canvas) {
val iconBounds = getBounds()
val factory = object : BaseIconFactory(
@@ -146,7 +208,7 @@ private fun NotificationEntry.extractPerson(): PersonModel? {
true) {}
val badge = factory.createBadgedIconBitmap(
appInfo.loadIcon(pm),
sbn.user,
user,
true,
appInfo.isInstantApp,
null)
@@ -156,7 +218,7 @@ private fun NotificationEntry.extractPerson(): PersonModel? {
colorFilter = drawable.colorFilter
val badgeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
16f,
15f,
context.resources.displayMetrics
).toInt()
setBounds(
@@ -181,8 +243,6 @@ private fun NotificationEntry.extractPerson(): PersonModel? {
@PixelFormat.Opacity
override fun getOpacity(): Int = PixelFormat.OPAQUE
}
return PersonModel(key, name, badgedAvatar, clickIntent)
}
private fun extractAvatarFromRow(entry: NotificationEntry): Drawable? =

View File

@@ -60,8 +60,9 @@ class PeopleHubSectionFooterViewAdapterImpl @Inject constructor(
private val dataSource: DataSource<@JvmSuppressWildcards PeopleHubViewModelFactory>
) : PeopleHubSectionFooterViewAdapter {
override fun bindView(viewBoundary: PeopleHubSectionFooterViewBoundary) =
dataSource.setListener(PeopleHubDataListenerImpl(viewBoundary))
override fun bindView(viewBoundary: PeopleHubSectionFooterViewBoundary) {
dataSource.registerListener(PeopleHubDataListenerImpl(viewBoundary))
}
}
private class PeopleHubDataListenerImpl(
@@ -92,8 +93,8 @@ class PeopleHubViewModelFactoryDataSourceImpl @Inject constructor(
private val dataSource: DataSource<@JvmSuppressWildcards PeopleHubModel>
) : DataSource<PeopleHubViewModelFactory> {
override fun setListener(listener: DataListener<PeopleHubViewModelFactory>) =
dataSource.setListener(PeopleHubModelListenerImpl(activityStarter, listener))
override fun registerListener(listener: DataListener<PeopleHubViewModelFactory>) =
dataSource.registerListener(PeopleHubModelListenerImpl(activityStarter, listener))
}
private class PeopleHubModelListenerImpl(

View File

@@ -0,0 +1,36 @@
/*
* 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.service.notification.StatusBarNotification
import javax.inject.Inject
import javax.inject.Singleton
interface PeopleNotificationIdentifier {
fun isPeopleNotification(sbn: StatusBarNotification): Boolean
}
@Singleton
class PeopleNotificationIdentifierImpl @Inject constructor(
private val personExtractor: NotificationPersonExtractor
) : PeopleNotificationIdentifier {
override fun isPeopleNotification(sbn: StatusBarNotification) =
sbn.notification.notificationStyle == Notification.MessagingStyle::class.java ||
personExtractor.extractPersonKey(sbn) != null
}

View File

@@ -28,10 +28,17 @@ fun <S, T> DataListener<T>.contraMap(mapper: (S) -> T): DataListener<S> = object
/** Boundary between a View and data pipeline, as seen by the View. */
interface DataSource<out T> {
fun setListener(listener: DataListener<T>)
fun registerListener(listener: DataListener<T>): Subscription
}
/** Represents a registration with a [DataSource]. */
interface Subscription {
/** Removes the previously registered [DataListener] from the [DataSource] */
fun unsubscribe()
}
/** 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))
override fun registerListener(listener: DataListener<T>) =
registerListener(listener.contraMap(mapper))
}

View File

@@ -131,6 +131,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
}
mInitialized = true;
reinflateViews(layoutInflater);
mPeopleHubViewAdapter.bindView(mPeopleHubViewBoundary);
mConfigurationController.addCallback(mConfigurationListener);
}
@@ -172,10 +173,6 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
if (oldPeopleHubPos != -1) {
mParent.addView(mPeopleHubView, oldPeopleHubPos);
}
if (!mInitialized) {
mPeopleHubViewAdapter.bindView(mPeopleHubViewBoundary);
}
}
/** Listener for when the "clear all" buttton is clciked on the gentle notification header. */

View File

@@ -57,7 +57,7 @@ public interface ExtensionController {
ExtensionBuilder<T> withCallback(Consumer<T> callback);
ExtensionBuilder<T> withUiMode(int mode, Supplier<T> def);
ExtensionBuilder<T> withFeature(String feature, Supplier<T> def);
Extension build();
Extension<T> build();
}
public interface PluginConverter<T, P> {

View File

@@ -135,7 +135,7 @@ public class ExtensionControllerImpl implements ExtensionController {
}
@Override
public ExtensionController.Extension build() {
public ExtensionController.Extension<T> build() {
// Sort items in ascending order
Collections.sort(mExtension.mProducers, Comparator.comparingInt(Item::sortOrder));
mExtension.notifyChanged();

View File

@@ -82,6 +82,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationRowBinder;
import com.android.systemui.statusbar.notification.collection.NotificationRowBinderImpl;
import com.android.systemui.statusbar.notification.logging.NotifLog;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -148,8 +149,12 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
private final CountDownLatch mCountDownLatch;
TestableNotificationEntryManager() {
super(new NotificationData(mock(NotificationSectionsFeatureManager.class),
mock(NotifLog.class)), mock(NotifLog.class));
super(
new NotificationData(
mock(NotificationSectionsFeatureManager.class),
mock(NotifLog.class),
mock(PeopleNotificationIdentifier.class)),
mock(NotifLog.class));
mCountDownLatch = new CountDownLatch(1);
}

View File

@@ -46,6 +46,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.notification.collection.NotificationData;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.NotifLog;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
@@ -78,8 +79,10 @@ public class NotificationListControllerTest extends SysuiTestCase {
// TODO: Remove this once EntryManager no longer needs to be mocked
private NotificationData mNotificationData =
new NotificationData(new NotificationSectionsFeatureManager(
new DeviceConfigProxyFake(), mContext), mock(NotifLog.class));
new NotificationData(
new NotificationSectionsFeatureManager(new DeviceConfigProxyFake(), mContext),
mock(NotifLog.class),
mock(PeopleNotificationIdentifier.class));
private int mNextNotifId = 0;

View File

@@ -81,6 +81,7 @@ import com.android.systemui.statusbar.SbnBuilder;
import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
import com.android.systemui.statusbar.notification.collection.NotificationData.KeyguardEnvironment;
import com.android.systemui.statusbar.notification.logging.NotifLog;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.ShadeController;
@@ -639,7 +640,10 @@ public class NotificationDataTest extends SysuiTestCase {
public static class TestableNotificationData extends NotificationData {
public TestableNotificationData(NotificationSectionsFeatureManager sectionsFeatureManager) {
super(sectionsFeatureManager, mock(NotifLog.class));
super(
sectionsFeatureManager,
mock(NotifLog.class),
mock(PeopleNotificationIdentifier.class));
}
public static final String OVERRIDE_RANK = "r";

View File

@@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.NotificationSectionsFeatureMa
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.collection.NotificationData;
import com.android.systemui.statusbar.notification.logging.NotifLog;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -236,8 +237,11 @@ public class NotificationPanelViewTest extends SysuiTestCase {
mock(PluginManager.class),
mock(ShadeController.class),
mock(NotificationLockscreenUserManager.class),
new NotificationEntryManager(new NotificationData(mock(
NotificationSectionsFeatureManager.class), mock(NotifLog.class)),
new NotificationEntryManager(
new NotificationData(
mock(NotificationSectionsFeatureManager.class),
mock(NotifLog.class),
mock(PeopleNotificationIdentifier.class)),
mock(NotifLog.class)),
mock(KeyguardStateController.class),
statusBarStateController,