com.google.firebase.database.core.ZombieEventManager Maven / Gradle / Ivy
/*
* Copyright 2017 Google Inc.
*
* 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.google.firebase.database.core;
import com.google.firebase.database.annotations.NotNull;
import com.google.firebase.database.core.view.QuerySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* {@link ZombieEventManager} records event registrations made from Query so that when they are
* unregistered, we immediately "zombie" them so that all further events are suppressed. This stops
* events that would normally be fired if events were already queued in the {@link Repo} or even in
* the Android message queue.
*/
public class ZombieEventManager implements EventRegistrationZombieListener {
private static ZombieEventManager defaultInstance = new ZombieEventManager();
// This hashmap stores the original eventregistrations sent to the repo.
// These are the registration instances that will get called with update events.
// Since EventRegistration overrides equals and hashcode, we create temporary instances
// to use as lookup keys.
// Package private for testing purposes only
final HashMap> globalEventRegistrations =
new HashMap<>();
private ZombieEventManager() {}
@NotNull
public static ZombieEventManager getInstance() {
return defaultInstance;
}
public void recordEventRegistration(EventRegistration registration) {
synchronized (globalEventRegistrations) {
List registrationList = globalEventRegistrations.get(registration);
if (registrationList == null) {
registrationList = new ArrayList<>();
globalEventRegistrations.put(registration, registrationList);
}
registrationList.add(registration);
// We record non default listeners twice, because when the default listener is zombied
// (removed) the repo will remove that listener on all specific queries as well.
// We need to match that behavior here and zombie all the relevant registrations.
if (!registration.getQuerySpec().isDefault()) {
EventRegistration defaultRegistration =
registration.clone(QuerySpec.defaultQueryAtPath(registration.getQuerySpec().getPath()));
registrationList = globalEventRegistrations.get(defaultRegistration);
if (registrationList == null) {
registrationList = new ArrayList<>();
globalEventRegistrations.put(defaultRegistration, registrationList);
}
registrationList.add(registration);
}
registration.setIsUserInitiated(true);
registration.setOnZombied(this);
}
}
private void unRecordEventRegistration(EventRegistration zombiedRegistration) {
synchronized (globalEventRegistrations) {
boolean found = false;
List registrationList = globalEventRegistrations.get(zombiedRegistration);
if (registrationList != null) {
for (int i = 0; i < registrationList.size(); i++) {
if (registrationList.get(i) == zombiedRegistration) {
found = true;
registrationList.remove(i);
break;
}
}
if (registrationList.isEmpty()) {
globalEventRegistrations.remove(zombiedRegistration);
}
}
assert (found || !zombiedRegistration.isUserInitiated());
// If the registration was recorded twice, we need to remove its second
// record.
if (!zombiedRegistration.getQuerySpec().isDefault()) {
EventRegistration defaultRegistration =
zombiedRegistration.clone(
QuerySpec.defaultQueryAtPath(zombiedRegistration.getQuerySpec().getPath()));
registrationList = globalEventRegistrations.get(defaultRegistration);
if (registrationList != null) {
for (int i = 0; i < registrationList.size(); i++) {
if (registrationList.get(i) == zombiedRegistration) {
registrationList.remove(i);
break;
}
}
if (registrationList.isEmpty()) {
globalEventRegistrations.remove(defaultRegistration);
}
}
}
}
}
public void zombifyForRemove(EventRegistration registration) {
synchronized (globalEventRegistrations) {
List registrationList = globalEventRegistrations.get(registration);
if (registrationList != null && !registrationList.isEmpty()) {
if (registration.getQuerySpec().isDefault()) {
// The behavior here has to match the behavior of SyncPoint
// .removeEventRegistration.
// If the query is default, it remove a single instance of the registration
// from each unique query. So for example, if you had 3 copies registered
// under default,
// you would end up with 2 still registered.
// If you had 1 registration in default and 2 in query a', you'd end up with
// just
// a single registration in a'.
// To implement this, we just store in a hashset queries that we remove so we
// can still
// keep a fairly simple structure.
// Note that we *could* use the same logic for non-default as the list there
// only has a
// a single query, but its somewhat wasteful to enumerate the list when we
// know we will
// only grab 1.
HashSet zombiedQueries = new HashSet<>();
// Walk down the list so that removes do not mess up the enumeration.
for (int i = registrationList.size() - 1; i >= 0; i--) {
EventRegistration currentRegistration = registrationList.get(i);
if (!zombiedQueries.contains(currentRegistration.getQuerySpec())) {
zombiedQueries.add(currentRegistration.getQuerySpec());
currentRegistration.zombify();
}
}
} else {
// Note that this entry cannot already be zombied because we are inside synchronization
// and any previous calls to zombify would have removed the entry.
registrationList.get(0).zombify();
}
}
}
}
@Override
public void onZombied(EventRegistration zombiedInstance) {
unRecordEventRegistration(zombiedInstance);
}
}