package com.mhacks.android.chat; import android.app.Activity; import android.content.res.Resources; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import com.firebase.client.ChildEventListener; import com.firebase.client.DataSnapshot; import com.firebase.client.FirebaseError; import com.firebase.client.Query; import com.mhacks.android.MainActivity; import com.mhacks.android.R; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * User: greg * Date: 6/21/13 * Time: 1:47 PM */ /** * This class is a generic way of backing an Android ListView with a Firebase location. * It handles all of the child events at the given Firebase location. It marshals received data into the given * class type. Extend this class and provide an implementation of <code>populateView</code>, which will be given an * instance of your list item layout and an instance your class that holds your data. Simply populate the view however * you like and this class will handle updating the list as the data changes. * @param <T> The class type to use as a model for the data contained in the children of the given Firebase location */ public abstract class FirebaseListAdapter<T> extends BaseAdapter { private Query ref; private Class<T> modelClass; private int layout; private LayoutInflater inflater; private List<T> models; private Map<String, T> modelNames; private ChildEventListener listener; private Activity mActivity; private long mInitTime; /** * @param ref The Firebase location to watch for data changes. Can also be a slice of a location, using some * combination of <code>limit()</code>, <code>startAt()</code>, and <code>endAt()</code>, * @param modelClass Firebase will marshall the data at a location into an instance of a class that you provide * @param layout This is the layout used to represent a single list item. You will be responsible for populating an * instance of the corresponding view with the data from an instance of modelClass. * @param activity The activity containing the ListView */ public FirebaseListAdapter(Query ref, Class<T> modelClass, int layout, Activity activity) { this.ref = ref; this.modelClass = modelClass; this.layout = layout; mActivity = activity; inflater = activity.getLayoutInflater(); models = new ArrayList<T>(); modelNames = new HashMap<String, T>(); // this variable gets set to -1 until the first child has been added, // at which point it stores the time at which that child was added. // This makes sure that Dave-detection doesn't overwhelm the user when // the fragment first loads. mInitTime = -1; // Look for all child events. We will then map them to our own internal ArrayList, which backs ListView listener = this.ref.addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { // check if this is the first child... there are better ways, but why not be safe if (mInitTime == -1) { mInitTime = new Date().getTime(); } T model = dataSnapshot.getValue(FirebaseListAdapter.this.modelClass); modelNames.put(dataSnapshot.getName(), model); // allows for Dave-inspired easteregg // if this chat message was sent more than one second after the adapter is loaded... if (model instanceof Chat && (new Date()).getTime() - mInitTime > 1000) { Chat chat = (Chat) model; if (chat.heKnows()) { Log.d("Lounge", chat.getUser() + " knows: " + chat.getMessage()); if (mActivity instanceof MainActivity) { final MainActivity main = (MainActivity) mActivity; main.hellYeah(); } } } // Insert into the correct location, based on previousChildName if (previousChildName == null) { models.add(0, model); } else { T previousModel = modelNames.get(previousChildName); int previousIndex = models.indexOf(previousModel); int nextIndex = previousIndex + 1; if (nextIndex == models.size()) { models.add(model); } else { models.add(nextIndex, model); } } notifyDataSetChanged(); } @Override public void onChildChanged(DataSnapshot dataSnapshot, String s) { // One of the models changed. Replace it in our list and name mapping String modelName = dataSnapshot.getName(); T oldModel = modelNames.get(modelName); T newModel = dataSnapshot.getValue(FirebaseListAdapter.this.modelClass); int index = models.indexOf(oldModel); models.set(index, newModel); modelNames.put(modelName, newModel); notifyDataSetChanged(); } @Override public void onChildRemoved(DataSnapshot dataSnapshot) { // A model was removed from the list. Remove it from our list and the name mapping String modelName = dataSnapshot.getName(); T oldModel = modelNames.get(modelName); models.remove(oldModel); modelNames.remove(modelName); notifyDataSetChanged(); } @Override public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { // A model changed position in the list. Update our list accordingly String modelName = dataSnapshot.getName(); T oldModel = modelNames.get(modelName); T newModel = dataSnapshot.getValue(FirebaseListAdapter.this.modelClass); int index = models.indexOf(oldModel); models.remove(index); if (previousChildName == null) { models.add(0, newModel); } else { T previousModel = modelNames.get(previousChildName); int previousIndex = models.indexOf(previousModel); int nextIndex = previousIndex + 1; if (nextIndex == models.size()) { models.add(newModel); } else { models.add(nextIndex, newModel); } } notifyDataSetChanged(); } @Override public void onCancelled(FirebaseError firebaseError) { Log.e("FirebaseListAdapter", "Listen was cancelled, no more updates will occur"); } }); } public void cleanup() { // We're being destroyed, let go of our listener and forget about all of the models ref.removeEventListener(listener); models.clear(); modelNames.clear(); } @Override public int getCount() { return models.size(); } @Override public Object getItem(int i) { return models.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(int i, View view, ViewGroup viewGroup) { if (view == null) { view = inflater.inflate(layout, viewGroup, false); } Resources res = view.getResources(); if (i % 2 == 1) { view.setBackgroundColor(res.getColor(R.color.translucent_gray)); } else { view.setBackgroundColor(res.getColor(android.R.color.transparent)); } T model = models.get(i); // Call out to subclass to marshall this model into the provided view populateView(view, model); return view; } /** * Each time the data at the given Firebase location changes, this method will be called for each item that needs * to be displayed. The arguments correspond to the layout and modelClass given to the constructor of this class. * * Your implementation should populate the view using the data contained in the model. * @param v The view to populate * @param model The object containing the data used to populate the view */ protected abstract void populateView(View v, T model); }