package org.ovirt.mobile.movirt.auth.properties.manager; import android.support.annotation.NonNull; import org.ovirt.mobile.movirt.auth.MovirtAuthenticator; import org.ovirt.mobile.movirt.auth.properties.AccountProperty; import org.ovirt.mobile.movirt.auth.properties.PropertyChangedListener; import org.ovirt.mobile.movirt.auth.properties.PropertyUtils; import org.ovirt.mobile.movirt.util.ObjectUtils; import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Only methods registerListener, notifyAndRegisterListener and notifyListener are safe to use from @AfterInject. * Other methods are NOT SAFE to call! Call isInitialized() for checking the state of this class. * It is not needed to check isInitialized() after all of the classes have been initialized. */ abstract class AccountPropertiesManagerCore { private static final String TAG = AccountPropertiesManagerCore.class.getSimpleName(); private static final String PROPERTY = "property"; private static final String LISTENER = "listener"; // this is not guaranteed to be set (circular injection) when this class has been already injected somewhere else protected MovirtAuthenticator authenticator; private boolean initialized = false; private static final Map<AccountProperty, Set<WrappedPropertyChangedListener>> listeners; private static Map<AccountProperty, Set<WrappedPropertyChangedListener>> initQueueListeners; static { listeners = new EnumMap<>(AccountProperty.class); initQueueListeners = new EnumMap<>(AccountProperty.class); for (AccountProperty property : AccountProperty.values()) { listeners.put(property, Collections.synchronizedSet(new HashSet<WrappedPropertyChangedListener>())); initQueueListeners.put(property, Collections.synchronizedSet(new HashSet<WrappedPropertyChangedListener>())); } } synchronized void setAuthenticator(MovirtAuthenticator authenticator) { // in very improbable case of multiple instances, AccountPropertiesManager is singleton if (this.authenticator != null) { return; } this.authenticator = authenticator; initialized = true; for (AccountProperty property : AccountProperty.values()) { notifyListeners(initQueueListeners.get(property), authenticator.getResource(property)); // should be run in current thread } initQueueListeners = null; // free } /** * Must be checked before any method (of this class) from @AfterInject of other classes, except for methods registerListener and * notifyAndRegisterListener or notifyListener which will be invoked the first time this component is initialized. * <p> * Other methods are NOT SAFE to call from @AfterInject! * * @return true if this component is initialized */ public boolean isInitialized() { return initialized; } /** * Calls * {@link AccountPropertiesManagerCore#notifyListener(PropertyChangedListener)} and * {@link AccountPropertiesManagerCore#registerListener(PropertyChangedListener)} */ public <E> void notifyAndRegisterListener(final PropertyChangedListener<E> listener) { notifyListener(listener); registerListener(listener); } /** * Notifies listener * The listener IS GUARANTEED to be called from current thread, UNLESS caller of this method created new thread in @AfterViews. * * @param listener listens for changes of property defined in {@link PropertyChangedListener#getProperty() getProperty}. * @throws ClassCastException if {@code <E>} doesn't correspond to {@link PropertyChangedListener#getProperty() getProperty}. */ @SuppressWarnings("unchecked") public <E> void notifyListener(final PropertyChangedListener<E> listener) { if (isInitialized()) { ObjectUtils.requireNotNull(listener.getProperty(), PROPERTY); ObjectUtils.requireNotNull(listener, LISTENER); listener.onPropertyChange((E) authenticator.getResource(listener.getProperty())); } else { registerListenerImpl(initQueueListeners, listener); } } /** * Registers listener. * The listener IS NOT GUARANTEED to be called from the main UI thread (listener will be called from * a thread specified by a caller of a setter of the property) * * @param listener listens for changes of property defined in {@link PropertyChangedListener#getProperty() getProperty} * @throws ClassCastException if {@code <E>} doesn't correspond to {@link PropertyChangedListener#getProperty() getProperty} * @see OnThread */ public <E> void registerListener(final PropertyChangedListener<E> listener) { registerListenerImpl(listeners, listener); } /** * @param listener to be removed from this manager * @return true if removed */ public boolean removeListener(final PropertyChangedListener listener) { if (listener == null) { return false; } WrappedPropertyChangedListener toRemove = new WrappedPropertyChangedListener() { @Override void onPropertyChange(Object o) { } @NonNull @Override PropertyChangedListener getListener() { return listener; } }; boolean result = false; for (Set<WrappedPropertyChangedListener> propertyListeners : listeners.values()) { result = propertyListeners.remove(toRemove) || result; } return result; } @SuppressWarnings("unchecked") private <E> void registerListenerImpl(Map<AccountProperty, Set<WrappedPropertyChangedListener>> listeners, final PropertyChangedListener<E> listener) { ObjectUtils.requireNotNull(listener.getProperty(), PROPERTY); ObjectUtils.requireNotNull(listener, LISTENER); listeners.get(listener.getProperty()).add(new WrappedPropertyChangedListener() { @Override public void onPropertyChange(Object newProperty) { listener.onPropertyChange((E) newProperty); } @NonNull @Override PropertyChangedListener getListener() { return listener; } }); } /** * @param property property to be checked against * @param object data to be checked against * @return true if property state is different than object */ public boolean propertyDiffers(AccountProperty property, Object object) { ObjectUtils.requireNotNull(property, PROPERTY); Object old = authenticator.getResource(property); return !PropertyUtils.propertyObjectEquals(old, object); } /** * @param property to be set and notified * @param object data to be set * @param runOnThread thread to fire the listeners on * @return true if property state changed and listeners were notified */ public boolean setAndNotify(AccountProperty property, Object object, OnThread runOnThread) { boolean propertyChanged = propertyDiffers(property, object); if (propertyChanged) { authenticator.setResource(property, object); if (!propertyDiffers(property, object)) { // setter worked notifyListeners(property, authenticator.getResource(property), runOnThread); // get set value notifyDependentProperties(property, runOnThread); } else { throw new IllegalStateException("Setter of account property " + property.name() + " doesn't set anything!"); } } return propertyChanged; } private void notifyDependentProperties(AccountProperty property, OnThread runOnThread) { for (AccountProperty prop : property.getDependentProperties()) { notifyListeners(prop, authenticator.getResource(prop), runOnThread); } } private void notifyListeners(AccountProperty property, Object o, OnThread runOnThread) { Set<WrappedPropertyChangedListener> propertyListeners = listeners.get(property); switch (runOnThread) { case CURRENT: notifyListeners(propertyListeners, o); break; case BACKGROUND: notifyBackgroundListeners(propertyListeners, o); break; } } void notifyListeners(Set<WrappedPropertyChangedListener> currentListeners, Object o) { if (currentListeners.isEmpty()) { return; } synchronized (currentListeners) { for (WrappedPropertyChangedListener listener : currentListeners) { listener.onPropertyChange(o); } } } /** * This method should not be used outside of {@link AccountPropertiesManager} */ abstract void notifyBackgroundListeners(Set<WrappedPropertyChangedListener> backgroundListeners, Object o); abstract class WrappedPropertyChangedListener { abstract void onPropertyChange(Object o); @NonNull abstract PropertyChangedListener getListener(); @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof WrappedPropertyChangedListener)) return false; WrappedPropertyChangedListener that = (WrappedPropertyChangedListener) o; return getListener().equals(that.getListener()); } @Override public int hashCode() { return getListener().hashCode(); } } }