in firebase-database/src/main/java/com/google/firebase/database/core/SyncTree.java [666:764]
private List<Event> removeEventRegistration(
final @NotNull QuerySpec query,
final @Nullable EventRegistration eventRegistration,
final @Nullable DatabaseError cancelError) {
return persistenceManager.runInTransaction(
new Callable<List<Event>>() {
@Override
public List<Event> call() {
// Find the syncPoint first. Then deal with whether or not it has matching listeners
Path path = query.getPath();
SyncPoint maybeSyncPoint = syncPointTree.get(path);
List<Event> cancelEvents = new ArrayList<Event>();
// A removal on a default query affects all queries at that location. A removal on an
// indexed query, even one without other query constraints, does *not* affect all
// queries at that location. So this check must be for 'default', and not
// loadsAllData().
if (maybeSyncPoint != null
&& (query.isDefault() || maybeSyncPoint.viewExistsForQuery(query))) {
// @type {{removed: !Array.<!fb.api.Query>, events: !Array.<!fb.core.view.Event>}}
Pair<List<QuerySpec>, List<Event>> removedAndEvents =
maybeSyncPoint.removeEventRegistration(query, eventRegistration, cancelError);
if (maybeSyncPoint.isEmpty()) {
syncPointTree = syncPointTree.remove(path);
}
List<QuerySpec> removed = removedAndEvents.getFirst();
cancelEvents = removedAndEvents.getSecond();
// We may have just removed one of many listeners and can short-circuit this whole
// process. We may also not have removed a default listener, in which case all of the
// descendant listeners should already be properly set up.
//
// Since indexed queries can shadow if they don't have other query constraints, check
// for loadsAllData(), instead of isDefault().
boolean removingDefault = false;
for (QuerySpec queryRemoved : removed) {
persistenceManager.setQueryInactive(query);
removingDefault = removingDefault || queryRemoved.loadsAllData();
}
ImmutableTree<SyncPoint> currentTree = syncPointTree;
boolean covered =
currentTree.getValue() != null && currentTree.getValue().hasCompleteView();
for (ChildKey component : path) {
currentTree = currentTree.getChild(component);
covered =
covered
|| (currentTree.getValue() != null
&& currentTree.getValue().hasCompleteView());
if (covered || currentTree.isEmpty()) {
break;
}
}
if (removingDefault && !covered) {
ImmutableTree<SyncPoint> subtree = syncPointTree.subtree(path);
// There are potentially child listeners. Determine what if any listens we need to
// send before executing the removal.
if (!subtree.isEmpty()) {
// We need to fold over our subtree and collect the listeners to send
List<View> newViews = collectDistinctViewsForSubTree(subtree);
// Ok, we've collected all the listens we need. Set them up.
for (View view : newViews) {
ListenContainer container = new ListenContainer(view);
QuerySpec newQuery = view.getQuery();
listenProvider.startListening(
queryForListening(newQuery), container.tag, container, container);
}
} else {
// There's nothing below us, so nothing we need to start listening on
}
}
// If we removed anything and we're not covered by a higher up listen, we need to stop
// listening on this query. The above block has us covered in terms of making sure
// we're set up on listens lower in the tree.
// Also, note that if we have a cancelError, it's already been removed at the provider
// level.
if (!covered && !removed.isEmpty() && cancelError == null) {
// If we removed a default, then we weren't listening on any of the other queries
// here. Just cancel the one default. Otherwise, we need to iterate through and
// cancel each individual query
if (removingDefault) {
listenProvider.stopListening(queryForListening(query), null);
} else {
for (QuerySpec queryToRemove : removed) {
Tag tag = tagForQuery(queryToRemove);
hardAssert(tag != null);
listenProvider.stopListening(queryForListening(queryToRemove), tag);
}
}
}
// Now, clear all of the tags we're tracking for the removed listens
removeTags(removed);
} else {
// No-op, this listener must've been already removed
}
return cancelEvents;
}
});
}