Revamp view manager logic to support any kind of view

Everything in the notif shade must now be represented by a controler
(specifically a NodeController). ExpandableNotificationRows are
NodeControllers, the NSSL is a NodeController (well, we wrap it in
something that is one), and, in the future, headers will have their own
controllers.

Split NotifViewManager into two pieces:
- ShadeViewManager, which consumes the shade list generated by the
  ShadeListBuilder and generates a "node list" -- instead of
  NotificationEntries, a tree of controllers (and their associated
  views). Plus, in the future, any header views.
- ShadeViewDiffer, which consumes a node list and applies any changes
  that need to be made to the view tree.

Test: atest
Change-Id: I016ff8c454ef7d18d4ac927c45ad132a3e5d4b13
This commit is contained in:
Ned Burns
2020-08-10 19:59:56 -04:00
parent 9bbf860b06
commit 2b69905817
15 changed files with 860 additions and 256 deletions

View File

@@ -307,7 +307,7 @@ public class PreparationCoordinator implements Coordinator {
private void onInflationFinished(NotificationEntry entry) {
mLogger.logNotifInflated(entry.getKey());
mInflatingNotifs.remove(entry);
mViewBarn.registerViewForEntry(entry, entry.getRow());
mViewBarn.registerViewForEntry(entry, entry.getRowController());
mInflationStates.put(entry, STATE_INFLATED);
mNotifInflatingFilter.invalidateList();
}

View File

@@ -136,6 +136,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
.expandableNotificationRow(row)
.notificationEntry(entry)
.onExpandClickListener(mPresenter)
.listContainer(mListContainer)
.build();
ExpandableNotificationRowController rowController =
component.getExpandableNotificationRowController();

View File

@@ -29,8 +29,7 @@ import com.android.systemui.statusbar.notification.collection.ShadeListBuilder;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
import com.android.systemui.statusbar.notification.collection.render.NotifViewManager;
import com.android.systemui.statusbar.notification.collection.render.NotifViewManagerBuilder;
import com.android.systemui.statusbar.notification.collection.render.ShadeViewManagerFactory;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import java.io.FileDescriptor;
@@ -51,7 +50,7 @@ public class NotifPipelineInitializer implements Dumpable {
private final NotifCoordinators mNotifPluggableCoordinators;
private final NotifInflaterImpl mNotifInflater;
private final DumpManager mDumpManager;
private final NotifViewManagerBuilder mNotifViewManagerBuilder;
private final ShadeViewManagerFactory mShadeViewManagerFactory;
private final FeatureFlags mFeatureFlags;
@@ -64,7 +63,7 @@ public class NotifPipelineInitializer implements Dumpable {
NotifCoordinators notifCoordinators,
NotifInflaterImpl notifInflater,
DumpManager dumpManager,
NotifViewManagerBuilder notifViewManagerBuilder,
ShadeViewManagerFactory shadeViewManagerFactory,
FeatureFlags featureFlags) {
mPipelineWrapper = pipelineWrapper;
mGroupCoalescer = groupCoalescer;
@@ -73,8 +72,8 @@ public class NotifPipelineInitializer implements Dumpable {
mNotifPluggableCoordinators = notifCoordinators;
mDumpManager = dumpManager;
mNotifInflater = notifInflater;
mShadeViewManagerFactory = shadeViewManagerFactory;
mFeatureFlags = featureFlags;
mNotifViewManagerBuilder = notifViewManagerBuilder;
}
/** Hooks the new pipeline up to NotificationManager */
@@ -95,8 +94,7 @@ public class NotifPipelineInitializer implements Dumpable {
// Wire up pipeline
if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
NotifViewManager notifViewManager = mNotifViewManagerBuilder.build(listContainer);
notifViewManager.attach(mListBuilder);
mShadeViewManagerFactory.create(listContainer).attach(mListBuilder);
}
mListBuilder.attach(mNotifCollection);
mNotifCollection.attach(mGroupCoalescer);

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection.render
import android.view.View
import java.lang.RuntimeException
import java.lang.StringBuilder
/**
* A controller that represents a single unit of addable/removable view(s) in the notification
* shade. Some nodes are just a single view (such as a header), while some might involve many views
* (such as a notification row).
*
* It's possible for nodes to support having child nodes (for example, some notification rows
* contain other notification rows). If so, they must implement all of the child-related methods
* below.
*/
interface NodeController {
/** A string that uniquely(ish) represents the node in the tree. Used for debugging. */
val nodeLabel: String
val view: View
fun getChildAt(index: Int): View? {
throw RuntimeException("Not supported")
}
fun getChildCount(): Int {
throw RuntimeException("Not supported")
}
fun addChildAt(child: NodeController, index: Int) {
throw RuntimeException("Not supported")
}
fun moveChildTo(child: NodeController, index: Int) {
throw RuntimeException("Not supported")
}
fun removeChild(child: NodeController, isTransfer: Boolean) {
throw RuntimeException("Not supported")
}
}
/**
* Used to specify the tree of [NodeController]s that currently make up the shade.
*/
interface NodeSpec {
val parent: NodeSpec?
val controller: NodeController
val children: List<NodeSpec>
}
class NodeSpecImpl(
override val parent: NodeSpec?,
override val controller: NodeController
) : NodeSpec {
override val children = mutableListOf<NodeSpec>()
}
/**
* Converts a tree spec to human-readable string, for dumping purposes.
*/
fun treeSpecToStr(tree: NodeSpec): String {
return StringBuilder().also { treeSpecToStrHelper(tree, it, "") }.toString()
}
private fun treeSpecToStrHelper(tree: NodeSpec, sb: StringBuilder, indent: String) {
sb.append("${indent}ns{${tree.controller.nodeLabel}")
if (tree.children.isNotEmpty()) {
val childIndent = "$indent "
for (child in tree.children) {
treeSpecToStrHelper(child, sb, childIndent)
}
}
}

View File

@@ -18,18 +18,19 @@ package com.android.systemui.statusbar.notification.collection.render
import android.view.textclassifier.Log
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController
import javax.inject.Inject
import javax.inject.Singleton
/**
* The ViewBarn is just a map from [ListEntry] to an instance of an [ExpandableNotificationRow].
* The ViewBarn is just a map from [ListEntry] to an instance of an
* [ExpandableNotificationRowController].
*/
@Singleton
class NotifViewBarn @Inject constructor() {
private val rowMap = mutableMapOf<String, ExpandableNotificationRow>()
private val rowMap = mutableMapOf<String, ExpandableNotificationRowController>()
fun requireView(forEntry: ListEntry): ExpandableNotificationRow {
fun requireView(forEntry: ListEntry): ExpandableNotificationRowController {
if (DEBUG) {
Log.d(TAG, "requireView: $forEntry.key")
}
@@ -41,11 +42,11 @@ class NotifViewBarn @Inject constructor() {
return li
}
fun registerViewForEntry(entry: ListEntry, view: ExpandableNotificationRow) {
fun registerViewForEntry(entry: ListEntry, controller: ExpandableNotificationRowController) {
if (DEBUG) {
Log.d(TAG, "registerViewForEntry: $entry.key")
}
rowMap[entry.key] = view
rowMap[entry.key] = controller
}
fun removeViewForEntry(entry: ListEntry) {

View File

@@ -1,240 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection.render
import android.annotation.MainThread
import android.view.View
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.ShadeListBuilder
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import javax.inject.Inject
/**
* A consumer of a Notification tree built by [ShadeListBuilder] which will update the notification
* presenter with the minimum operations required to make the old tree match the new one
*/
@MainThread
class NotifViewManager constructor(
private val listContainer: NotificationListContainer,
private val viewBarn: NotifViewBarn,
private val logger: NotifViewManagerLogger
) {
private val rootNode = RootWrapper(listContainer)
private val rows = mutableMapOf<ListEntry, RowNode>()
fun attach(listBuilder: ShadeListBuilder) {
listBuilder.setOnRenderListListener(::onNewNotifTree)
}
private fun onNewNotifTree(tree: List<ListEntry>) {
// Step 1: Detach all views whose parents have changed
detachRowsWithModifiedParents()
// Step 2: Attach all new views and reattach all views whose parents changed.
// Also reorder existing children to match the spec we've received
val orderChanged = addAndReorderChildren(rootNode, tree)
if (orderChanged) {
listContainer.generateChildOrderChangedEvent()
}
}
private fun detachRowsWithModifiedParents() {
val toRemove = mutableListOf<ListEntry>()
for (row in rows.values) {
val oldParentEntry = row.nodeParent?.entry
val newParentEntry = row.entry.parent
if (newParentEntry != oldParentEntry) {
// If the parent is null, then we should remove the child completely. If not, then
// the parent merely changed: we'll detach it for now and then attach it to the
// new parent in step 2.
val isTransfer = newParentEntry != null
if (!isTransfer) {
toRemove.add(row.entry)
}
if (!isTransfer && !isAttachedToRootEntry(oldParentEntry)) {
// If our view parent has also been removed (i.e. is no longer attached to the
// root entry) then we skip removing the child here
logger.logSkippingDetach(row.entry.key, row.nodeParent?.entry?.key)
} else {
logger.logDetachingChild(
row.entry.key,
isTransfer,
oldParentEntry?.key,
newParentEntry?.key)
row.nodeParent?.removeChild(row, isTransfer)
row.nodeParent = null
}
}
}
rows.keys.removeAll(toRemove)
}
private fun addAndReorderChildren(parent: ParentNode, childEntries: List<ListEntry>): Boolean {
var orderChanged = false
for ((index, entry) in childEntries.withIndex()) {
val row = getRowNode(entry)
val currView = parent.getChildViewAt(index)
if (currView != row.view) {
when (row.nodeParent) {
null -> {
logger.logAttachingChild(row.entry.key, parent.entry.key)
parent.addChildAt(row, index)
row.nodeParent = parent
}
parent -> {
logger.logMovingChild(row.entry.key, parent.entry.key, index)
parent.moveChild(row, index)
orderChanged = true
}
else -> {
throw IllegalStateException("Child ${row.entry.key} should have parent " +
"${parent.entry.key} but is actually " +
"${row.nodeParent?.entry?.key}")
}
}
}
if (row is GroupWrapper) {
val childOrderChanged = addAndReorderChildren(row, row.entry.children)
orderChanged = orderChanged || childOrderChanged
}
}
// TODO: setUntruncatedChildCount
return orderChanged
}
private fun getRowNode(entry: ListEntry): RowNode {
return rows.getOrPut(entry) {
when (entry) {
is NotificationEntry -> RowWrapper(entry, viewBarn.requireView(entry))
is GroupEntry ->
GroupWrapper(
entry,
viewBarn.requireView(checkNotNull(entry.summary)),
listContainer)
else -> throw RuntimeException(
"Unexpected entry type for ${entry.key}: ${entry.javaClass}")
}
}
}
}
class NotifViewManagerBuilder @Inject constructor(
private val viewBarn: NotifViewBarn,
private val logger: NotifViewManagerLogger
) {
fun build(listContainer: NotificationListContainer): NotifViewManager {
return NotifViewManager(listContainer, viewBarn, logger)
}
}
private fun isAttachedToRootEntry(entry: ListEntry?): Boolean {
return when (entry) {
null -> false
ROOT_ENTRY -> true
else -> isAttachedToRootEntry(entry.parent)
}
}
private interface Node {
val entry: ListEntry
val nodeParent: ParentNode?
}
private interface ParentNode : Node {
fun getChildViewAt(index: Int): View?
fun addChildAt(child: RowNode, index: Int)
fun moveChild(child: RowNode, index: Int)
fun removeChild(child: RowNode, isTransfer: Boolean)
}
private interface RowNode : Node {
val view: ExpandableNotificationRow
override var nodeParent: ParentNode?
}
private class RootWrapper(
private val listContainer: NotificationListContainer
) : ParentNode {
override val entry: ListEntry = ROOT_ENTRY
override val nodeParent: ParentNode? = null
override fun getChildViewAt(index: Int): View? {
return listContainer.getContainerChildAt(index)
}
override fun addChildAt(child: RowNode, index: Int) {
listContainer.addContainerViewAt(child.view, index)
}
override fun moveChild(child: RowNode, index: Int) {
listContainer.changeViewPosition(child.view, index)
}
override fun removeChild(child: RowNode, isTransfer: Boolean) {
if (isTransfer) {
listContainer.setChildTransferInProgress(true)
}
listContainer.removeContainerView(child.view)
if (isTransfer) {
listContainer.setChildTransferInProgress(false)
}
}
}
private class GroupWrapper(
override val entry: GroupEntry,
override val view: ExpandableNotificationRow,
val listContainer: NotificationListContainer
) : RowNode, ParentNode {
override var nodeParent: ParentNode? = null
override fun getChildViewAt(index: Int): View? {
return view.getChildNotificationAt(index)
}
override fun addChildAt(child: RowNode, index: Int) {
view.addChildNotification(child.view, index)
listContainer.notifyGroupChildAdded(child.view)
}
override fun moveChild(child: RowNode, index: Int) {
view.removeChildNotification(child.view)
view.addChildNotification(child.view, index)
}
override fun removeChild(child: RowNode, isTransfer: Boolean) {
view.removeChildNotification(child.view)
if (isTransfer) {
listContainer.notifyGroupChildRemoved(child.view, view)
}
}
}
private class RowWrapper(
override val entry: NotificationEntry,
override val view: ExpandableNotificationRow
) : RowNode {
override var nodeParent: ParentNode? = null
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection.render
import android.view.View
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
/**
* Temporary wrapper around [NotificationListContainer], for use by [ShadeViewDiffer]. Long term,
* we should just modify NLC to implement the NodeController interface.
*/
class RootNodeController(
private val listContainer: NotificationListContainer
) : NodeController {
override val nodeLabel: String = "<root>"
override val view: View = listContainer as View
override fun getChildAt(index: Int): View? {
return listContainer.getContainerChildAt(index)
}
override fun getChildCount(): Int {
return listContainer.containerChildCount
}
override fun addChildAt(child: NodeController, index: Int) {
listContainer.addContainerViewAt(child.view, index)
}
override fun moveChildTo(child: NodeController, index: Int) {
listContainer.changeViewPosition(child.view as ExpandableView, index)
}
override fun removeChild(child: NodeController, isTransfer: Boolean) {
if (isTransfer) {
listContainer.setChildTransferInProgress(true)
}
listContainer.removeContainerView(child.view)
if (isTransfer) {
listContainer.setChildTransferInProgress(false)
}
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection.render
import android.annotation.MainThread
import android.view.View
import com.android.systemui.util.kotlin.transform
/**
* Given a "spec" that describes a "tree" of views, adds and removes views from the
* [rootController] and its children until the actual tree matches the spec.
*
* Every node in the spec tree must specify both a view and its associated [NodeController].
* Commands to add/remove/reorder children are sent to the controller. How the controller
* interprets these commands is left to its own discretion -- it might add them directly to its
* associated view or to some subview container.
*
* It's possible for nodes to mix "unmanaged" views in alongside managed ones within the same
* container. In this case, whenever the differ runs it will move all unmanaged views to the end
* of the node's child list.
*/
@MainThread
class ShadeViewDiffer(
rootController: NodeController,
private val logger: ShadeViewDifferLogger
) {
private val rootNode = ShadeNode(rootController)
private val nodes = mutableMapOf(rootController to rootNode)
private val views = mutableMapOf<View, ShadeNode>()
/**
* Adds and removes views from the root (and its children) until their structure matches the
* provided [spec]. The root node of the spec must match the root controller passed to the
* differ's constructor.
*/
fun applySpec(spec: NodeSpec) {
val specMap = treeToMap(spec)
if (spec.controller != rootNode.controller) {
throw IllegalArgumentException("Tree root ${spec.controller.nodeLabel} does not " +
"match own root at ${rootNode.label}")
}
detachChildren(rootNode, specMap)
attachChildren(rootNode, specMap)
}
/**
* If [view] is managed by this differ, then returns the label of the view's controller.
* Otherwise returns View.toString().
*
* For debugging purposes.
*/
fun getViewLabel(view: View): String {
return views[view]?.label ?: view.toString()
}
private fun detachChildren(
parentNode: ShadeNode,
specMap: Map<NodeController, NodeSpec>
) {
val parentSpec = specMap[parentNode.controller]
for (i in parentNode.getChildCount() - 1 downTo 0) {
val childView = parentNode.getChildAt(i)
views[childView]?.let { childNode ->
val childSpec = specMap[childNode.controller]
maybeDetachChild(parentNode, parentSpec, childNode, childSpec)
if (childNode.controller.getChildCount() > 0) {
detachChildren(childNode, specMap)
}
}
}
}
private fun maybeDetachChild(
parentNode: ShadeNode,
parentSpec: NodeSpec?,
childNode: ShadeNode,
childSpec: NodeSpec?
) {
val newParentNode = transform(childSpec?.parent) { getNode(it) }
if (newParentNode != parentNode) {
val childCompletelyRemoved = newParentNode == null
if (childCompletelyRemoved) {
nodes.remove(childNode.controller)
views.remove(childNode.controller.view)
}
if (childCompletelyRemoved && parentSpec == null) {
// If both the child and the parent are being removed at the same time, then
// keep the child attached to the parent for animation purposes
logger.logSkippingDetach(childNode.label, parentNode.label)
} else {
logger.logDetachingChild(
childNode.label,
!childCompletelyRemoved,
parentNode.label,
newParentNode?.label)
parentNode.removeChild(childNode, !childCompletelyRemoved)
childNode.parent = null
}
}
}
private fun attachChildren(
parentNode: ShadeNode,
specMap: Map<NodeController, NodeSpec>
) {
val parentSpec = checkNotNull(specMap[parentNode.controller])
for ((index, childSpec) in parentSpec.children.withIndex()) {
val currView = parentNode.getChildAt(index)
val childNode = getNode(childSpec)
if (childNode.view != currView) {
when (childNode.parent) {
null -> {
// A new child (either newly created or coming from some other parent)
logger.logAttachingChild(childNode.label, parentNode.label)
parentNode.addChildAt(childNode, index)
childNode.parent = parentNode
}
parentNode -> {
// A pre-existing child, just in the wrong position. Move it into place
logger.logMovingChild(childNode.label, parentNode.label, index)
parentNode.moveChildTo(childNode, index)
}
else -> {
// Error: child still has a parent. We should have detached it in the
// previous step.
throw IllegalStateException("Child ${childNode.label} should have " +
"parent ${parentNode.label} but is actually " +
"${childNode.parent?.label}")
}
}
}
if (childSpec.children.isNotEmpty()) {
attachChildren(childNode, specMap)
}
}
}
private fun getNode(spec: NodeSpec): ShadeNode {
var node = nodes[spec.controller]
if (node == null) {
node = ShadeNode(spec.controller)
nodes[node.controller] = node
views[node.view] = node
}
return node
}
private fun treeToMap(tree: NodeSpec): Map<NodeController, NodeSpec> {
val map = mutableMapOf<NodeController, NodeSpec>()
registerNodes(tree, map)
return map
}
private fun registerNodes(node: NodeSpec, map: MutableMap<NodeController, NodeSpec>) {
if (map.containsKey(node.controller)) {
throw RuntimeException("Node ${node.controller.nodeLabel} appears more than once")
}
map[node.controller] = node
if (node.children.isNotEmpty()) {
for (child in node.children) {
registerNodes(child, map)
}
}
}
}
private class ShadeNode(
val controller: NodeController
) {
val view = controller.view
var parent: ShadeNode? = null
val label: String
get() = controller.nodeLabel
fun getChildAt(index: Int): View? = controller.getChildAt(index)
fun getChildCount(): Int = controller.getChildCount()
fun addChildAt(child: ShadeNode, index: Int) {
controller.addChildAt(child.controller, index)
}
fun moveChildTo(child: ShadeNode, index: Int) {
controller.moveChildTo(child.controller, index)
}
fun removeChild(child: ShadeNode, isTransfer: Boolean) {
controller.removeChild(child.controller, isTransfer)
}
}

View File

@@ -21,7 +21,7 @@ import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.NotificationLog
import javax.inject.Inject
class NotifViewManagerLogger @Inject constructor(
class ShadeViewDifferLogger @Inject constructor(
@NotificationLog private val buffer: LogBuffer
) {
fun logDetachingChild(

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection.render
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.ShadeListBuilder
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import java.lang.RuntimeException
import javax.inject.Inject
/**
* Responsible for building and applying the "shade node spec": the list (tree) of things that
* currently populate the notification shade.
*/
class ShadeViewManager constructor(
listContainer: NotificationListContainer,
logger: ShadeViewDifferLogger,
private val viewBarn: NotifViewBarn
) {
private val rootController = RootNodeController(listContainer)
private val viewDiffer = ShadeViewDiffer(rootController, logger)
fun attach(listBuilder: ShadeListBuilder) {
listBuilder.setOnRenderListListener(::onNewNotifTree)
}
private fun onNewNotifTree(tree: List<ListEntry>) {
viewDiffer.applySpec(buildTree(tree))
}
private fun buildTree(notifList: List<ListEntry>): NodeSpec {
val root = NodeSpecImpl(null, rootController)
for (entry in notifList) {
// TODO: Add section header logic here
root.children.add(buildNotifNode(entry, root))
}
return root
}
private fun buildNotifNode(entry: ListEntry, parent: NodeSpec): NodeSpec {
return when (entry) {
is NotificationEntry -> {
NodeSpecImpl(parent, viewBarn.requireView(entry))
}
is GroupEntry -> {
val groupNode = NodeSpecImpl(
parent,
viewBarn.requireView(checkNotNull(entry.summary)))
for (childEntry in entry.children) {
groupNode.children.add(buildNotifNode(childEntry, groupNode))
}
groupNode
}
else -> {
throw RuntimeException("Unexpected entry: $entry")
}
}
}
}
class ShadeViewManagerFactory @Inject constructor(
private val logger: ShadeViewDifferLogger,
private val viewBarn: NotifViewBarn
) {
fun create(listContainer: NotificationListContainer): ShadeViewManager {
return ShadeViewManager(listContainer, logger, viewBarn)
}
}

View File

@@ -22,21 +22,27 @@ import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENAB
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.notification.collection.render.NodeController;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.row.dagger.AppName;
import com.android.systemui.statusbar.notification.row.dagger.NotificationKey;
import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.time.SystemClock;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
@@ -44,8 +50,9 @@ import javax.inject.Named;
* Controller for {@link ExpandableNotificationRow}.
*/
@NotificationRowScope
public class ExpandableNotificationRowController {
public class ExpandableNotificationRowController implements NodeController {
private final ExpandableNotificationRow mView;
private final NotificationListContainer mListContainer;
private final ActivatableNotificationViewController mActivatableNotificationViewController;
private final NotificationMediaManager mMediaManager;
private final PluginManager mPluginManager;
@@ -72,6 +79,7 @@ public class ExpandableNotificationRowController {
@Inject
public ExpandableNotificationRowController(ExpandableNotificationRow view,
NotificationListContainer listContainer,
ActivatableNotificationViewController activatableNotificationViewController,
NotificationMediaManager mediaManager, PluginManager pluginManager,
SystemClock clock, @AppName String appName, @NotificationKey String notificationKey,
@@ -86,6 +94,7 @@ public class ExpandableNotificationRowController {
OnDismissCallback onDismissCallback, FalsingManager falsingManager,
PeopleNotificationIdentifier peopleNotificationIdentifier) {
mView = view;
mListContainer = listContainer;
mActivatableNotificationViewController = activatableNotificationViewController;
mMediaManager = mediaManager;
mPluginManager = pluginManager;
@@ -162,4 +171,52 @@ public class ExpandableNotificationRowController {
private void logNotificationExpansion(String key, boolean userAction, boolean expanded) {
mNotificationLogger.onExpansionChanged(key, userAction, expanded);
}
@Override
@NonNull
public String getNodeLabel() {
return mView.getEntry().getKey();
}
@Override
@NonNull
public View getView() {
return mView;
}
@Override
public View getChildAt(int index) {
return mView.getChildNotificationAt(index);
}
@Override
public void addChildAt(NodeController child, int index) {
ExpandableNotificationRow childView = (ExpandableNotificationRow) child.getView();
mView.addChildNotification((ExpandableNotificationRow) child.getView());
mListContainer.notifyGroupChildAdded(childView);
}
@Override
public void moveChildTo(NodeController child, int index) {
ExpandableNotificationRow childView = (ExpandableNotificationRow) child.getView();
mView.removeChildNotification(childView);
mView.addChildNotification(childView, index);
}
@Override
public void removeChild(NodeController child, boolean isTransfer) {
ExpandableNotificationRow childView = (ExpandableNotificationRow) child.getView();
mView.removeChildNotification(childView);
if (!isTransfer) {
mListContainer.notifyGroupChildRemoved(childView, mView);
}
}
@Override
public int getChildCount() {
final List<ExpandableNotificationRow> mChildren = mView.getAttachedChildren();
return mChildren != null ? mChildren.size() : 0;
}
}

View File

@@ -25,6 +25,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.phone.StatusBar;
import dagger.Binds;
@@ -55,6 +56,8 @@ public interface ExpandableNotificationRowComponent {
Builder notificationEntry(NotificationEntry entry);
@BindsInstance
Builder onExpandClickListener(ExpandableNotificationRow.OnExpandClickListener presenter);
@BindsInstance
Builder listContainer(NotificationListContainer listContainer);
ExpandableNotificationRowComponent build();
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.kotlin
/**
* If [value] is not null, then returns block(value). Otherwise returns null.
*/
inline fun <T : Any, R> transform(value: T?, block: (T) -> R): R? = value?.let(block)

View File

@@ -0,0 +1,304 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection.render;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class ShadeViewDifferTest extends SysuiTestCase {
private ShadeViewDiffer mDiffer;
private FakeController mRootController = new FakeController(mContext, "RootController");
private FakeController mController1 = new FakeController(mContext, "Controller1");
private FakeController mController2 = new FakeController(mContext, "Controller2");
private FakeController mController3 = new FakeController(mContext, "Controller3");
private FakeController mController4 = new FakeController(mContext, "Controller4");
private FakeController mController5 = new FakeController(mContext, "Controller5");
private FakeController mController6 = new FakeController(mContext, "Controller6");
private FakeController mController7 = new FakeController(mContext, "Controller7");
@Mock
ShadeViewDifferLogger mLogger;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mDiffer = new ShadeViewDiffer(mRootController, mLogger);
}
@Test
public void testAddInitialViews() {
// WHEN a spec is applied to an empty root
// THEN the final tree matches the spec
applySpecAndCheck(
node(mController1),
node(mController2,
node(mController3),
node(mController4)
),
node(mController5)
);
}
@Test
public void testDetachViews() {
// GIVEN a preexisting tree of controllers
applySpecAndCheck(
node(mController1),
node(mController2,
node(mController3),
node(mController4)
),
node(mController5)
);
// WHEN the new spec removes nodes
// THEN the final tree matches the spec
applySpecAndCheck(
node(mController5)
);
}
@Test
public void testReparentChildren() {
// GIVEN a preexisting tree of controllers
applySpecAndCheck(
node(mController1),
node(mController2,
node(mController3),
node(mController4)
),
node(mController5)
);
// WHEN the parents of the controllers are all shuffled around
// THEN the final tree matches the spec
applySpecAndCheck(
node(mController1),
node(mController4),
node(mController3,
node(mController2)
)
);
}
@Test
public void testReorderChildren() {
// GIVEN a preexisting tree of controllers
applySpecAndCheck(
node(mController1),
node(mController2),
node(mController3),
node(mController4)
);
// WHEN the children change order
// THEN the final tree matches the spec
applySpecAndCheck(
node(mController3),
node(mController2),
node(mController4),
node(mController1)
);
}
@Test
public void testRemovedGroupsAreKeptTogether() {
// GIVEN a preexisting tree with a group
applySpecAndCheck(
node(mController1),
node(mController2,
node(mController3),
node(mController4),
node(mController5)
)
);
// WHEN the new spec removes the entire group
applySpecAndCheck(
node(mController1)
);
// THEN the group children are still attached to their parent
assertEquals(mController2.getView(), mController3.getView().getParent());
assertEquals(mController2.getView(), mController4.getView().getParent());
assertEquals(mController2.getView(), mController5.getView().getParent());
}
@Test
public void testUnmanagedViews() {
// GIVEN a preexisting tree of controllers
applySpecAndCheck(
node(mController1),
node(mController2,
node(mController3),
node(mController4)
),
node(mController5)
);
// GIVEN some additional unmanaged views attached to the tree
View unmanagedView1 = new View(mContext);
View unmanagedView2 = new View(mContext);
mRootController.getView().addView(unmanagedView1, 1);
mController2.getView().addView(unmanagedView2, 0);
// WHEN a new spec is applied with additional nodes
// THEN the final tree matches the spec
applySpecAndCheck(
node(mController1),
node(mController2,
node(mController3),
node(mController4),
node(mController6)
),
node(mController5),
node(mController7)
);
// THEN the unmanaged views have been pushed to the end of their parents
assertEquals(unmanagedView1, mRootController.view.getChildAt(4));
assertEquals(unmanagedView2, mController2.view.getChildAt(3));
}
private void applySpecAndCheck(NodeSpec spec) {
mDiffer.applySpec(spec);
checkMatchesSpec(spec);
}
private void applySpecAndCheck(SpecBuilder... children) {
applySpecAndCheck(node(mRootController, children).build());
}
private void checkMatchesSpec(NodeSpec spec) {
final NodeController parent = spec.getController();
final List<NodeSpec> children = spec.getChildren();
for (int i = 0; i < children.size(); i++) {
NodeSpec childSpec = children.get(i);
View view = parent.getChildAt(i);
assertEquals(
"Child " + i + " of parent " + parent.getNodeLabel() + " should be "
+ childSpec.getController().getNodeLabel() + " but is instead "
+ (view != null ? mDiffer.getViewLabel(view) : "null"),
view,
childSpec.getController().getView());
if (!childSpec.getChildren().isEmpty()) {
checkMatchesSpec(childSpec);
}
}
}
private static class FakeController implements NodeController {
public final FrameLayout view;
private final String mLabel;
FakeController(Context context, String label) {
view = new FrameLayout(context);
mLabel = label;
}
@NonNull
@Override
public String getNodeLabel() {
return mLabel;
}
@NonNull
@Override
public FrameLayout getView() {
return view;
}
@Override
public int getChildCount() {
return view.getChildCount();
}
@Override
public View getChildAt(int index) {
return view.getChildAt(index);
}
@Override
public void addChildAt(@NonNull NodeController child, int index) {
view.addView(child.getView(), index);
}
@Override
public void moveChildTo(@NonNull NodeController child, int index) {
view.removeView(child.getView());
view.addView(child.getView(), index);
}
@Override
public void removeChild(@NonNull NodeController child, boolean isTransfer) {
view.removeView(child.getView());
}
}
private static class SpecBuilder {
private final NodeController mController;
private final SpecBuilder[] mChildren;
SpecBuilder(NodeController controller, SpecBuilder... children) {
mController = controller;
mChildren = children;
}
public NodeSpec build() {
return build(null);
}
public NodeSpec build(@Nullable NodeSpec parent) {
final NodeSpecImpl spec = new NodeSpecImpl(parent, mController);
for (SpecBuilder childBuilder : mChildren) {
spec.getChildren().add(childBuilder.build(spec));
}
return spec;
}
}
private static SpecBuilder node(NodeController controller, SpecBuilder... children) {
return new SpecBuilder(controller, children);
}
}

View File

@@ -221,6 +221,7 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase {
.thenAnswer((Answer<ExpandableNotificationRowController>) invocation ->
new ExpandableNotificationRowController(
viewCaptor.getValue(),
mListContainer,
mock(ActivatableNotificationViewController.class),
mNotificationMediaManager,
mock(PluginManager.class),