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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
|
||||
.expandableNotificationRow(row)
|
||||
.notificationEntry(entry)
|
||||
.onExpandClickListener(mPresenter)
|
||||
.listContainer(mListContainer)
|
||||
.build();
|
||||
ExpandableNotificationRowController rowController =
|
||||
component.getExpandableNotificationRowController();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user