/** * Wire * Copyright (C) 2016 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.waz.zclient.core.api.scala; import android.support.annotation.NonNull; import com.waz.api.UiObservable; import com.waz.api.UpdateListener; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import static com.waz.zclient.core.api.scala.ModelObserver.Reason.FORCED_UPDATE; import static com.waz.zclient.core.api.scala.ModelObserver.Reason.INTERNAL_CHANGE; import static com.waz.zclient.core.api.scala.ModelObserver.Reason.NEW_MODEL; public abstract class ModelObserver<T extends UiObservable> { public enum Reason { NEW_MODEL, INTERNAL_CHANGE, FORCED_UPDATE } /** * A set of observers for each model that we want to listen to in the collection of models passed to us. We only * want one observer for any given model object. * * protected for testing purposes */ protected Set<SingleModelObserver> observers; public ModelObserver() { observers = new HashSet<>(); } /** * @see #setAndUpdate(Collection) * @param model */ public void setAndUpdate(T model) { //avoids possibly heap pollution. setAndUpdate(Collections.singletonList(model)); } /** * @see #setAndUpdate(Collection) * @param models */ public void setAndUpdate(T[] models) { setAndUpdate(Arrays.asList(models)); } /** * <p> * For a given list of {@link UiObservable} 'models', register an observer to each one and call update on that observer. * When any of the observers is updated, then it will call through to {@link #updated(UiObservable)} with the model * that was updated, in effect funnelling all of the update listeners into one method call. * </p> * * <p> * Note, this method clears any previous {@link SingleModelObserver}s that this {@link ModelObserver} was using, * before creating new observers for any models passed in. If we were previously watching models A and B, and then * call #setAndUpdate on models B and C, this {@link ModelObserver} will then be listening to B and C, and NOT A. * Furthermore, a call to update on B will NOT be performed, as we were already listening to it. * </p> * <p> * If a particular model in the collection is null, then nothing will happen. * </p> * @param models */ public void setAndUpdate(Collection<T> models) { pauseListening(); Set<SingleModelObserver> newModels = createObserverCollection(models); if (!observers.isEmpty() && !observers.retainAll(newModels)) { resumeListening(); return; } resumeListening(); newModels.removeAll(observers); for (SingleModelObserver observer : newModels) { observer.startListening(); } observers.addAll(newModels); } /** * @see #setAndPause(Collection) * @param model */ public void setAndPause(T model) { //avoids possibly heap pollution. setAndPause(Collections.singletonList(model)); } public void setAndPause(Collection<T> models) { pauseListening(); Set<SingleModelObserver> newModels = createObserverCollection(models); if (!observers.isEmpty() && !observers.retainAll(newModels)) { resumeListening(); return; } resumeListening(); observers.addAll(newModels); } /** * Create a set of SingleModelObservers for a collection of Models, but don't yet update them */ private Set<SingleModelObserver> createObserverCollection(Collection<T> models) { Set<SingleModelObserver> set = new HashSet<>(); for (T model : models) { if (model != null) { set.add(new SingleModelObserver(model)); } } return set; } /** * @see #addAndUpdate(Collection) * @param model */ public void addAndUpdate(T model) { addAndUpdate(Collections.singletonList(model)); } /** * @see #addAndUpdate(Collection) * @param models */ public void addAndUpdate(T[] models) { addAndUpdate(Arrays.asList(models)); } /** * <p> * Add an extra model to the set of models that this {@link ModelObserver} watches and call update only to those models * added. If a particular model was already being watched by this {@link ModelObserver}, then the particular observer * for that model will be replaced with a new one, and update will be called on it. * </p> * <p> * Note, this method preserves the observers of any models added previously that are NOT being replaced. If models A * and B were being listened to already, and we call #addAndUpdate on a model C, then this {@link ModelObserver} * will be observer A, B and C. If the model was already being listened to by this particular {@link ModelObserver}, * then the observer for that model will be replaced by a new one and update will be called again. * </p> * <p> * If a particular model in the collection is null, then nothing will happen. * </p> * @param models */ public void addAndUpdate(Collection<T> models) { for (T model : models) { addModelAndUpdate(model); } } private void addModelAndUpdate(T model) { if (model == null) { return; } SingleModelObserver observer = new SingleModelObserver(model); if (observers.add(observer)) { observer.startListening(); } //else the model was already being observed, do nothing } /** * For any calls to {@link #pauseListening()} that were made, resume listening again. */ public void resumeListening() { for (SingleModelObserver observer : observers) { observer.resumeListening(); } } public String debugCurentModels() { StringBuilder sb = new StringBuilder(); for (SingleModelObserver observer : observers) { sb.append(String.format("listening: %s, to %s", observer.listening, observer.model)); } return sb.toString(); } /** * <p> * If there any models that this {@link ModelObserver} was observing, then pause listening to it. This method does * not destroy the observers that were watching the models. This is useful if we need to pause listening briefly to * prevent a UI element from being updated when we don't want it to be. * </p> * <p> * Note, this method doesn't do any clean up. It does not set the references to the observers (which in turn hold * references to models) to null. As such, calling this method in the clean-up methods of other components has little * purpose aside from assuring that no calls to update will occur during that clean up process. * </p> */ public void pauseListening() { for (SingleModelObserver observer : observers) { observer.pauseListening(); } } /** * Remove any observers and clear the references to them in case we want to re-use the {@link ModelObserver}. For * example, if we are listening to models A and B, we have a new set of models B and C and we want to stop listening * to A, we must first call clear before registering to B and C. */ public void clear() { pauseListening(); observers.clear(); } /** * Cause all observed models to have their observers updated. * This will result in a call to {@link #updated(UiObservable)} for every model this ModelObserver is watching. */ public void forceUpdate() { for (SingleModelObserver observer : observers) { observer.updated(FORCED_UPDATE); } } /** * Use this update method if you don't care why the model updated * @param model the model that has been updated for whatever reason */ public void updated(T model) { } /** * Use this update method if you want to perform different logic for different update reasons. This should prevent * you having to keep reference to the model in your own classes as much as possible. * @param model the model that has been updated * @param reason the reason the model was updated */ public void updated(T model, Reason reason) { } /** * Protected for testing purposes */ protected final class SingleModelObserver implements UpdateListener { private final T model; protected boolean listening = false; SingleModelObserver(@NonNull T model) { this.model = model; } public void startListening() { resumeListening(); updated(NEW_MODEL); } public void resumeListening() { model.addUpdateListener(this); listening = true; } public void pauseListening() { model.removeUpdateListener(this); listening = false; } @Override public void updated() { updated(INTERNAL_CHANGE); } public void updated(Reason reason) { ModelObserver.this.updated(model); ModelObserver.this.updated(model, reason); } /** * Protected for testing purposes * * @return */ protected T getModel() { return model; } /** * Returns the model's equals result so that we can maintain a one-to-one mapping of {@link SingleModelObserver}s * to {@link UiObservable} objects. * @param o * @return */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SingleModelObserver that = (SingleModelObserver) o; return model.equals(that.model); } /** * @see #equals(Object) * @return */ @Override public int hashCode() { return model.hashCode(); } } }