/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * Granite Data Services 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 Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or see <http://www.gnu.org/licenses/>. */ package org.granite.binding.android; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.WeakHashMap; import org.granite.binding.ObservableValue; import org.granite.binding.PropertyChangeHelper; import org.granite.binding.WritableObservableValue; import android.app.Activity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; /** * @author William DRAI */ public class Binder { private final Map<View, ViewWatcher<? extends View>> viewWatchers = new WeakHashMap<View, ViewWatcher<? extends View>>(); private final List<Binding> bindings = new ArrayList<Binding>(); private final Activity activity; private BeanResolver beanResolver = new DefaultBeanResolver(); private List<IdGetter> idGetters = new ArrayList<IdGetter>(); private final Map<Class<?>, IdGetter> idGettersByClass = new HashMap<Class<?>, IdGetter>(); public Binder(Activity activity) { this.activity = activity; initListeners(); } public void setBeanResolver(BeanResolver beanResolver) { this.beanResolver = beanResolver; } public void registerIdGetter(IdGetter idGetter) { idGetters.add(idGetter); } public interface BeanResolver { public <T> T resolveBean(Object ref); public <T> BeanSetter<T> getBeanSetter(T bean, String propertyName); } private static final class DefaultBeanResolver implements BeanResolver { @SuppressWarnings("unchecked") public <T> T resolveBean(Object ref) { return (T)ref; } public <T> BeanSetter<T> getBeanSetter(T bean, String propertyName) { return new MethodBeanSetter<T>(bean, propertyName); } } public long getId(Object instance) { if (instance == null) return 0L; IdGetter idGetter = idGettersByClass.get(instance.getClass()); if (idGetter == null) { for (IdGetter g : idGetters) { if (g.accepts(instance)) { idGetter = g; break; } } if (idGetter != null) idGettersByClass.put(instance.getClass(), idGetter); } if (idGetter == null) throw new RuntimeException("Cannot get id for object " + instance + ", register an IdGetter implementation"); return idGetter.getId(instance); } public void bind(View view, String viewPropertyName, Object ref, String beanPropertyName) { bindings.add(new UnidirectionalBinding<Object>(view, viewPropertyName, beanResolver.resolveBean(ref), beanPropertyName)); } @SuppressWarnings("unchecked") public <T> void bind(View view, String viewPropertyName, Object ref, String beanProperty, Getter<T, ?> beanPropertyGetter) { bindings.add(new UnidirectionalBinding<T>(view, viewPropertyName, (T)beanResolver.resolveBean(ref), beanProperty, beanPropertyGetter)); } public <T> void bind(View view, String viewPropertyName, ObservableValue observableValue) { bindings.add(new UnidirectionalBinding<T>(view, viewPropertyName, observableValue)); } public void unbind(View view, String viewPropertyName) { unbind(UnidirectionalBinding.class, view, viewPropertyName); } public void bind(int viewId, String viewPropertyName, Object ref, String beanPropertyName) { bindings.add(new UnidirectionalBinding<Object>(activity.findViewById(viewId), viewPropertyName, beanResolver.resolveBean(ref), beanPropertyName)); } @SuppressWarnings("unchecked") public <T> void bind(int viewId, String viewPropertyName, Object ref, String beanPropertyName, Getter<T, ?> beanPropertyGetter) { bindings.add(new UnidirectionalBinding<T>(activity.findViewById(viewId), viewPropertyName, (T)beanResolver.resolveBean(ref), beanPropertyName, beanPropertyGetter)); } public <T> void bind(int viewId, String viewPropertyName, ObservableValue observableValue) { bindings.add(new UnidirectionalBinding<T>(activity.findViewById(viewId), viewPropertyName, observableValue)); } public void unbind(int viewId, String viewPropertyName) { unbind(UnidirectionalBinding.class, activity.findViewById(viewId), viewPropertyName); } public void bind(View rootView, int viewId, String viewPropertyName, Object ref, String beanPropertyName) { bindings.add(new UnidirectionalBinding<Object>(rootView.findViewById(viewId), viewPropertyName, beanResolver.resolveBean(ref), beanPropertyName)); } @SuppressWarnings("unchecked") public <T> void bind(View rootView, int viewId, String viewPropertyName, Object ref, String beanPropertyName, Getter<T, ?> beanPropertyGetter) { bindings.add(new UnidirectionalBinding<T>(rootView.findViewById(viewId), viewPropertyName, (T)beanResolver.resolveBean(ref), beanPropertyName, beanPropertyGetter)); } public <T> void bind(View rootView, int viewId, String viewPropertyName, ObservableValue observableValue) { bindings.add(new UnidirectionalBinding<T>(rootView.findViewById(viewId), viewPropertyName, observableValue)); } public void unbind(View rootView, int viewId, String viewPropertyName) { unbind(UnidirectionalBinding.class, rootView.findViewById(viewId), viewPropertyName); } public void bindBidirectional(View view, String viewPropertyName, Object ref, String beanPropertyName) { bindings.add(new BidirectionalBinding(view, viewPropertyName, beanResolver.resolveBean(ref), beanPropertyName)); } public void unbindBidirectional(View view, String viewPropertyName) { unbind(BidirectionalBinding.class, view, viewPropertyName); } public void bindBidirectional(int viewId, String viewPropertyName, Object ref, String beanPropertyName) { bindings.add(new BidirectionalBinding(activity.findViewById(viewId), viewPropertyName, beanResolver.resolveBean(ref), beanPropertyName)); } public void unbindBidirectional(int viewId, String viewPropertyName) { unbind(BidirectionalBinding.class, activity.findViewById(viewId), viewPropertyName); } public void bindBidirectional(View rootView, int viewId, String viewPropertyName, Object ref, String beanPropertyName) { bindings.add(new BidirectionalBinding(rootView.findViewById(viewId), viewPropertyName, beanResolver.resolveBean(ref), beanPropertyName)); } public void unbindBidirectional(View rootView, int viewId, String viewPropertyName) { unbind(BidirectionalBinding.class, rootView.findViewById(viewId), viewPropertyName); } public void unbindAll(View view) { for (Iterator<Binding> ibinding = bindings.iterator(); ibinding.hasNext(); ) { Binding binding = ibinding.next(); if (binding.isFor(view)) { binding.unbind(); ibinding.remove(); } } } private void unbind(Class<? extends Binding> type, View view, String viewProperty) { for (Iterator<Binding> ibinding = bindings.iterator(); ibinding.hasNext(); ) { Binding binding = ibinding.next(); if (binding.getClass() == type && binding.isFor(view, viewProperty)) { binding.unbind(); ibinding.remove(); } } } public void applyBindings() { for (Entry<View, ViewWatcher<? extends View>> entry : viewWatchers.entrySet()) entry.getValue().apply(); } public void bind(View rootView) { bind(rootView, null); } public void bind(View rootView, Object item) { if (rootView == null) return; if (rootView.getTag() instanceof String) internalBind(rootView, item); if (rootView instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup)rootView; for (int i = 0; i < viewGroup.getChildCount(); i++) bind(viewGroup.getChildAt(i), item); } } public void bind(Menu menu) { for (int i = 0; i < menu.size(); i++) { MenuItem menuItem = menu.getItem(i); if (menuItem.getSubMenu() != null) bind(menuItem.getSubMenu()); if (menuItem.getActionView() != null) bind(menuItem.getActionView()); } } public void unbind(View rootView) { if (rootView.getTag() instanceof String) internalUnbind(rootView); if (rootView instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup)rootView; for (int i = 0; i < viewGroup.getChildCount(); i++) unbind(viewGroup.getChildAt(i)); } } public void unbind(Menu menu) { for (int i = 0; i < menu.size(); i++) { MenuItem menuItem = menu.getItem(i); if (menuItem.getSubMenu() != null) unbind(menuItem.getSubMenu()); if (menuItem.getActionView() != null) unbind(menuItem.getActionView()); } } private void internalBind(View view, Object item) { String[] bindings = ((String)view.getTag()).split("\\,"); for (String binding : bindings) { int idx = binding.indexOf("="); if (idx < 0) continue; String viewProperty = binding.substring(0, idx); if (binding.charAt(idx+1) == '#' || binding.charAt(idx+1) == '$') { // Data binding boolean bidir = binding.charAt(idx+1) == '#'; binding = binding.substring(idx+2); int idx2 = binding.lastIndexOf("."); Object beanRef = binding.substring(0, idx2); String beanProperty = binding.substring(idx2+1); if ("item".equals(beanRef) && item != null) beanRef = item; if (bidir) bindBidirectional(view, viewProperty, beanRef, beanProperty); else bind(view, viewProperty, beanRef, beanProperty); } else { // Listener binding binding = binding.substring(idx+1); int idx2 = binding.lastIndexOf("."); Object beanRef = binding.substring(0, idx2); String beanMethodName = binding.substring(idx2+1); bindListener(view, viewProperty, beanRef, beanMethodName); } } } private void internalUnbind(View view) { String[] bindings = ((String)view.getTag()).split("\\,"); for (String binding : bindings) { int idx = binding.indexOf("="); if (idx < 0) continue; String viewProperty = binding.substring(0, idx); if (binding.charAt(idx+1) == '#' || binding.charAt(idx+1) == '$') { // Data binding unbind(view, viewProperty); } else { // Listener binding unbindListener(view, viewProperty); } } } private int ACTION_BINDING_TAG = 348623214; private Map<Class<?>, Object> listenersMap = new HashMap<Class<?>, Object>(); private void initListeners() { listenersMap.put(View.OnClickListener.class, new View.OnClickListener() { @Override public void onClick(View v) { applyBindings(); MethodAction action = (MethodAction)v.getTag(ACTION_BINDING_TAG); if (action != null) action.invokeAction(); } }); } private InvocationHandler listenerInvocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getDeclaringClass() == Object.class || !method.getName().startsWith("on")) return method.invoke(proxy, args); View v = (View)args[0]; MethodAction action = (MethodAction)v.getTag(ACTION_BINDING_TAG); if (action != null && method.getName().equals(action.getListenerMethod())) { applyBindings(); Object[] args2 = new Object[args.length-1]; if (args.length > 1) System.arraycopy(args, 1, args2, 0, args.length-1); return action.invokeAction(args2); } return null; } }; private class MethodAction implements InvocationHandler { private final Object bean; private final String beanMethod; private final String listenerMethod; public MethodAction(Object bean, String beanMethod, String listenerMethod) { this.bean = bean; this.beanMethod = beanMethod; this.listenerMethod = listenerMethod; } public String getListenerMethod() { return listenerMethod; } public Object invokeAction(Object... args) { try { for (Method m : bean.getClass().getMethods()) { if (m.getName().equals(beanMethod)) { if (m.getParameterTypes().length == args.length) return m.invoke(bean, args); else if (m.getParameterTypes().length < args.length) { Object[] args2 = new Object[m.getParameterTypes().length]; if (args2.length > 0) System.arraycopy(args, 0, args2, 0, args2.length); return m.invoke(bean, args2); } } } return null; } catch (Exception e) { throw new RuntimeException("Cannot invoke method action " + beanMethod, e); } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getDeclaringClass() == Object.class) return method.invoke(this, args); if (!method.getName().equals(listenerMethod)) return false; // Indicate not handled applyBindings(); Object ret = invokeAction(args); if (ret == null) return true; return ret; } } public void bindListener(int viewId, String viewEvent, Object ref, String beanMethodName) { bindListener(activity.findViewById(viewId), viewEvent, ref, beanMethodName); } public void bindListener(View rootView, int viewId, String viewEvent, Object ref, String beanMethodName) { bindListener(rootView.findViewById(viewId), viewEvent, ref, beanMethodName); } public void bindListener(View view, String viewEvent, Object ref, String beanMethodName) { String listenerMethodName = viewEvent; int idx = viewEvent.indexOf("."); if (idx > 0) { listenerMethodName = viewEvent.substring(0, idx) + viewEvent.substring(idx+1, idx+2).toUpperCase() + viewEvent.substring(idx+2); viewEvent = viewEvent.substring(0, idx); } listenerMethodName = "on" + listenerMethodName.substring(0, 1).toUpperCase() + listenerMethodName.substring(1); String methodName = "setOn" + viewEvent.substring(0, 1).toUpperCase() + viewEvent.substring(1) + "Listener"; Method method = null; for (Method m : view.getClass().getMethods()) { if (m.getName().equals(methodName)) { method = m; break; } } if (method == null) throw new RuntimeException("Not setter found for event " + viewEvent + " on view " + view); Object listener = null; MethodAction methodAction = new MethodAction(beanResolver.resolveBean(ref), beanMethodName, listenerMethodName); boolean withView = false; Class<?> listenerClass = method.getParameterTypes()[0]; for (Method m : listenerClass.getMethods()) { if (m.getName().equals(listenerMethodName)) { withView = m.getParameterTypes().length > 0 && View.class.isAssignableFrom(m.getParameterTypes()[0]); break; } } if (withView) { view.setTag(ACTION_BINDING_TAG, methodAction); listener = listenersMap.get(method.getParameterTypes()[0]); if (listener == null) { listener = Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] { method.getParameterTypes()[0] }, listenerInvocationHandler); listenersMap.put(method.getParameterTypes()[0], listener); } } else listener = Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] { method.getParameterTypes()[0] }, methodAction); try { method.invoke(view, listener); } catch (Exception e) { throw new RuntimeException("Could not set listener for event " + viewEvent + " on view " + view.getClass().getSimpleName(), e); } } public void unbindListener(int viewId, String viewEvent) { unbindListener(activity.findViewById(viewId), viewEvent); } public void unbindListener(View rootView, int viewId, String viewEvent) { unbindListener(rootView.findViewById(viewId), viewEvent); } public void unbindListener(View view, String viewEvent) { String methodName = "setOn" + viewEvent.substring(0, 1).toUpperCase() + viewEvent.substring(1) + "Listener"; Method method = null; for (Method m : view.getClass().getMethods()) { if (m.getName().equals(methodName)) { method = m; break; } } if (method == null) throw new RuntimeException("Not setter found for event " + viewEvent + " on view " + view); view.setTag(ACTION_BINDING_TAG, null); try { method.invoke(view, (Object)null); } catch (Exception e) { throw new RuntimeException("Could not unset listener for event " + viewEvent + " on view " + view.getClass().getSimpleName(), e); } } private static class MethodGetter<B, V> implements Getter<B, V> { private final Method getter; public MethodGetter(B bean, String beanPropertyName) { Method getter = null; try { getter = bean.getClass().getMethod("get" + beanPropertyName.substring(0, 1).toUpperCase() + beanPropertyName.substring(1)); } catch (NoSuchMethodException e) { try { getter = bean.getClass().getMethod("is" + beanPropertyName.substring(0, 1).toUpperCase() + beanPropertyName.substring(1)); } catch (NoSuchMethodException e2) { throw new RuntimeException("Could not find getter on bean " + bean.getClass().getName() + "." + beanPropertyName, e); } } this.getter = getter; } @SuppressWarnings("unchecked") public V getValue(B instance) throws Exception { return (V)getter.invoke(instance); } } private static class MethodBeanSetter<B> implements BeanSetter<B> { private final Method setter; public MethodBeanSetter(B bean, String beanPropertyName) { Method beanSetter = null; try { for (Method m : bean.getClass().getMethods()) { if (m.getName().equals("set" + beanPropertyName.substring(0, 1).toUpperCase() + beanPropertyName.substring(1)) && m.getParameterTypes().length == 1) { beanSetter = m; break; } } } catch (Exception e) { throw new RuntimeException("Could not find setter on bean " + bean.getClass().getName() + "." + beanPropertyName, e); } if (beanSetter == null) throw new RuntimeException("Could not find setter on bean " + bean.getClass().getName() + "." + beanPropertyName); this.setter = beanSetter; } @Override public void setValue(B instance, String beanPropertyName, Object value) throws Exception { setter.invoke(instance, value); } } public interface Binding { public boolean isFor(View view, String viewProperty); public boolean isFor(View view); public void unbind(); } private class UnidirectionalBinding<B> implements Binding { private final WeakReference<View> viewRef; private final String viewPropertyName; private final ViewSetter<View> viewSetter; private final ObservableValue observableValue; public UnidirectionalBinding(View view, String viewPropertyName, B bean, String beanPropertyName) { this(view, viewPropertyName, bean, beanPropertyName, null); } public UnidirectionalBinding(View view, String viewPropertyName, B bean, String beanPropertyName, Getter<B, ?> beanPropertyGetter) { this(view, viewPropertyName, beanPropertyGetter != null ? new ObservableBeanProperty<B>(bean, beanPropertyName, beanPropertyGetter) : new ObservableBeanProperty<B>(bean, beanPropertyName) ); } @SuppressWarnings("unchecked") public UnidirectionalBinding(View view, String viewPropertyName, ObservableValue observableValue) { if (view == null) throw new NullPointerException("Cannot bind on null view"); if (viewPropertyName == null) throw new NullPointerException("Cannot bind on null viewPropertyName"); if (observableValue == null) throw new NullPointerException("Cannot bind on null observableValue"); this.viewRef = new WeakReference<View>(view); this.viewSetter = (ViewSetter<View>)ViewBindingRegistry.getViewSetter(view.getClass(), viewPropertyName); this.viewPropertyName = viewPropertyName; this.observableValue = observableValue; this.observableValue.addChangeListener(observableValueChangeListener); setViewValue(this.observableValue.getValue()); } public void unbind() { this.observableValue.removeChangeListener(observableValueChangeListener); } private void setViewValue(Object newValue) { View view = viewRef.get(); if (view != null) viewSetter.setValue(view, newValue); } private PropertyChangeListener observableValueChangeListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { Object newValue = event.getNewValue(); setViewValue(newValue); } }; public boolean isFor(View view, String viewPropertyName) { return viewRef.get() == view && viewPropertyName.equals(this.viewPropertyName); } public boolean isFor(View view) { return viewRef.get() == view; } } private class BidirectionalBinding implements Binding { private final WeakReference<View> viewRef; private final ViewSetter<View> viewSetter; private final String viewPropertyName; private final WritableObservableValue beanProperty; private boolean changing = false; @SuppressWarnings("unchecked") public BidirectionalBinding(View view, String viewPropertyName, Object bean, String beanPropertyName) { if (view == null) throw new NullPointerException("Cannot bind on null view"); if (viewPropertyName == null) throw new NullPointerException("Cannot bind on null viewPropertyName"); if (bean == null) throw new NullPointerException("Cannot bind on null bean"); if (beanPropertyName == null) throw new NullPointerException("Cannot bind on null beanPropertyName"); this.viewRef = new WeakReference<View>(view); this.viewSetter = (ViewSetter<View>)ViewBindingRegistry.getViewSetter(view.getClass(), viewPropertyName); this.beanProperty = new BeanProperty<Object>(bean, beanPropertyName); this.beanProperty.addChangeListener(beanPropertyChangeListener); setViewValue(beanProperty.getValue()); ViewWatcher<? extends View> viewWatcher = viewWatchers.get(view); if (viewWatcher == null) { viewWatcher = ViewBindingRegistry.newWatcher(view); viewWatchers.put(view, viewWatcher); } viewWatcher.addPropertyChangeListener(viewPropertyName, viewPropertyChangeListener); this.viewPropertyName = viewPropertyName; } public void unbind() { beanProperty.removeChangeListener(beanPropertyChangeListener); View view = viewRef.get(); if (view != null) { ViewWatcher<? extends View> viewWatcher = viewWatchers.get(view); if (viewWatcher != null) viewWatcher.removePropertyChangeListener(viewPropertyName, viewPropertyChangeListener); if (viewWatcher.isEmpty()) viewWatchers.remove(view); } } private void setViewValue(Object newValue) { if (viewRef.get() != null) viewSetter.setValue(viewRef.get(), newValue); } public boolean isFor(View view, String viewPropertyName) { return viewRef.get() == view && viewPropertyName.equals(this.viewPropertyName); } public boolean isFor(View view) { return viewRef.get() == view; } private PropertyChangeListener viewPropertyChangeListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { if (changing) return; try { changing = true; Object newValue = event.getNewValue(); beanProperty.setValue(newValue); } finally { changing = false; } } }; private PropertyChangeListener beanPropertyChangeListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { if (changing) return; try { changing = true; Object newValue = event.getNewValue(); setViewValue(newValue); } finally { changing = false; } } }; } private class ObservableBeanProperty<B> implements ObservableValue { protected final WeakReference<B> beanRef; protected final String beanPropertyName; private final Getter<B, ?> beanGetter; public ObservableBeanProperty(B bean, String beanPropertyName) { this.beanRef = new WeakReference<B>(bean); this.beanPropertyName = beanPropertyName; if (bean instanceof Map<?, ?>) this.beanGetter = null; else this.beanGetter = new MethodGetter<B, Object>(bean, beanPropertyName); } public ObservableBeanProperty(B bean, String beanPropertyName, Getter<B, ?> beanGetter) { this.beanRef = new WeakReference<B>(bean); this.beanPropertyName = beanPropertyName; this.beanGetter = beanGetter; } public void addChangeListener(PropertyChangeListener listener) { if (beanRef.get() != null) PropertyChangeHelper.addPropertyChangeListener(beanRef.get(), beanPropertyName, listener); } public void removeChangeListener(PropertyChangeListener listener) { if (beanRef.get() != null) PropertyChangeHelper.removePropertyChangeListener(beanRef.get(), beanPropertyName, listener); } @SuppressWarnings("unchecked") @Override public Object getValue() { if (beanRef.get() != null) { try { if (beanGetter == null) return ((Map<String, ?>)beanRef.get()).get(beanPropertyName); return beanGetter.getValue(beanRef.get()); } catch (Exception e) { throw new RuntimeException("Could not get bean property value " + beanPropertyName, e); } } return null; } } private class BeanProperty<B> extends ObservableBeanProperty<B> implements WritableObservableValue { private final BeanSetter<B> beanSetter; public BeanProperty(B bean, String beanPropertyName) { super(bean, beanPropertyName); if (bean instanceof Map<?, ?>) this.beanSetter = null; else this.beanSetter = beanResolver.getBeanSetter(bean, beanPropertyName); } @SuppressWarnings("unchecked") public void setValue(Object value) { if (beanRef.get() != null) { try { if (beanSetter == null) ((Map<String, Object>)beanRef.get()).put(beanPropertyName, value); else this.beanSetter.setValue(beanRef.get(), beanPropertyName, value); } catch (Exception e) { throw new RuntimeException("Could not set bean property value " + beanPropertyName, e); } } } } }