Skip to content

Instantly share code, notes, and snippets.

@emanuelet
Created April 11, 2016 04:41
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save emanuelet/b8c0049fc29702bc2fdbd493b385e7b2 to your computer and use it in GitHub Desktop.
Save emanuelet/b8c0049fc29702bc2fdbd493b385e7b2 to your computer and use it in GitHub Desktop.
Populating ListView (or RecyclerView) based on Firebase cross-referenced results with a Geofire node
public class ExampleFragment extends Fragment implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
ResultCallback<LocationSettingsResult>,
SharedPreferences.OnSharedPreferenceChangeListener {
private Callbacks mCallbacks;
private Snackbar snackbar;
public interface Callbacks {
/**
* Callback for when an item has been selected.
*/
public void onItemSelected(Item deal, View view);
}
/**
* A dummy implementation of the {@link Callbacks} interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
private static Callbacks sDummyCallbacks = new Callbacks() {
@Override
public void onItemSelected(Item deal, View view) {
}
};
/**
* The desired interval for location updates. Inexact. Updates may be more or less frequent.
*/
public static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000;
/**
* The fastest rate for active location updates. Exact. Updates will never be more frequent
* than this value.
*/
public static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS =
UPDATE_INTERVAL_IN_MILLISECONDS / 2;
public static final String GEOFIRE_CHILD = "geofire";
public static final String DEALS_CHILD = "items";
protected static final int REQUEST_CHECK_SETTINGS = 0x1;
private static final String SELECTED_KEY = "selected_position";
private final String LOG_TAG = ExampleFragment.class.getSimpleName();
public Firebase mFirebaseRef;
/**
* Stores parameters for requests to the FusedLocationProviderApi.
*/
protected LocationRequest mLocationRequest;
/**
* Stores the types of location services the client is interested in using. Used for checking
* settings to determine if the device has optimal location settings.
*/
protected LocationSettingsRequest mLocationSettingsRequest;
//Main Screen Views
ListView mListView;
@InjectView(R.id.MainLayout)
LinearLayout mMainLayout;
@InjectView(R.id.list_container)
MultiStateView mListContainer;
@InjectView(R.id.header_msg)
TextView mHeader;
SharedPreferences settings;
// global variables
@Icicle
int mPosition = ListView.INVALID_POSITION;
GeoLocation center;
@Icicle
boolean mActiveGeoQuery = false;
private GeoFire geoFire;
private GeoQuery query;
private View rootView;
private ItemListAdapter mItemListAdapter;
private int counter = 0;
/* Data from the authenticated user */
private AuthData mAuthData;
/* Client used to interact with Google APIs. */
private GoogleApiClient mGoogleApiClient;
private Location mLastLocation;
private double mLatitudeText;
private double mLongitudeText;
private Context mContext;
public ExampleFragment() {
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(LOG_TAG, "onCreate");
super.onCreate(savedInstanceState);
mContext = getActivity();
settings = PreferenceManager.getDefaultSharedPreferences(mContext);
setHasOptionsMenu(true);
mFirebaseRef = new Firebase(getString(R.string.firebase_url)).child(DEALS_CHILD);
//I initialize the object that will be used to retrieve the location
buildGoogleApiClient();
buildLocationSettingsRequest();
checkLocationSettings();
geoFire = new GeoFire(new Firebase(getString(R.string.firebase_url)).child(GEOFIRE_CHILD));
mItemListAdapter = new ItemListAdapter(mFirebaseRef.equalTo(GEOFIRE_CHILD), getActivity(), R.layout.list_item_items);
}
protected synchronized void buildGoogleApiClient() {
Log.d(LOG_TAG, "buildGoogleApiClient");
mGoogleApiClient = new GoogleApiClient.Builder(mContext)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
}
@Override
public void onDestroy() {
super.onDestroy();
settings.unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onStop() {
super.onStop();
if (mGoogleApiClient.isConnected()) {
mGoogleApiClient.disconnect();
}
}
@Override
public void onStart() {
super.onStart();
Log.d(LOG_TAG, "onStart");
mGoogleApiClient.connect();
//TODO: check if location is activated
center = new GeoLocation(PrefsUtils.getPreferredLat(mContext), PrefsUtils.getPreferredLong(mContext));
if (center.latitude != 0 && center.longitude != 0 && !mActiveGeoQuery) {
startGeoQuery();
} else if (mActiveGeoQuery) {
Log.d(LOG_TAG, "geoquery already active");
} else {
Log.d(LOG_TAG, "center not setted");
//I first try to set the center at the Last Known Location if retrieved
if (Double.isNaN(mLatitudeText) && mLatitudeText != 0) {
Log.d(LOG_TAG, "center setted from fused location");
center = new GeoLocation(mLatitudeText, mLongitudeText);
startGeoQuery();
}
}
checkForItems();
}
@Override
public void onResume() {
super.onResume();
Log.d(LOG_TAG, "onResume");
center = new GeoLocation(PrefsUtils.getPreferredLat(mContext), PrefsUtils.getPreferredLong(mContext));
if (center.latitude != 0 && center.longitude != 0) {
Log.d(LOG_TAG, "onResume center setted");
mItemListAdapter.notifyDataSetChanged();
} else {
Log.d(LOG_TAG, "onResume center not setted");
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_main, container, false);
ButterKnife.inject(this, rootView);
mListView = (ListView) mListContainer.getContentView();
mListContainer.setState(MultiStateView.ContentState.LOADING);
mListContainer.setEmptyString(getString(R.string.empty_list_description));
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
Item cursor = (Item) mListView.getItemAtPosition(i);
if (cursor != null) {
mCallbacks.onItemSelected(cursor, view);
}
mPosition = i;
}
});
return rootView;
}
private void bindListView() {
mListView.setAdapter(mItemListAdapter);
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Log.d(LOG_TAG, "preference " + key + " change");
if (query != null) {
if (key.equals(getString(R.string.search_radius_key))) {
String newRadius = PrefsUtils.getSearchadius(mContext);
Log.d(LOG_TAG, "new radius " + newRadius);
query.setRadius(Double.parseDouble(newRadius));
}
// when the location will change it will trigger the change on the latitude and longitude preference
// I hook up the change of the center in the query to longitude that will be fired last
if (key.equals(getString(R.string.long_pref_key))) {
GeoLocation newCenter = new GeoLocation(PrefsUtils.getPreferredLat(mContext), PrefsUtils.getPreferredLong(mContext));
Log.d(LOG_TAG, "new center " + newCenter.toString());
query.setCenter(newCenter);
}
if (key.equals(getString(R.string.category_pref_key))) {
String filter = PrefsUtils.getCategoryFilter(mContext);
Log.d(LOG_TAG, "filter: " + filter);
mItemListAdapter.getFilter().filter(filter, new Filter.FilterListener() {
@Override
public void onFilterComplete(int count) {
//wait until the the filtering is completed to change the state of the main view
isListEmpty();
}
});
// I update the header view
if (!filter.isEmpty()) {
mHeader.setVisibility(View.VISIBLE);
mHeader.setText(filter);
} else {
mHeader.setVisibility(View.GONE);
}
}
}
}
private void startGeoQuery() {
double radius = Double.parseDouble(PrefsUtils.getSearchadius(mContext));
// safety net in case that for some reasons the returned radius is 0
if (radius < 1.0) {
radius = 2.0;
}
query = geoFire.queryAtLocation(center, radius);
Log.d(LOG_TAG, "center: " + center.toString() + ", radius: " + radius);
query.addGeoQueryEventListener(new GeoQueryEventListener() {
@Override
public void onKeyEntered(String key, GeoLocation location) {
Log.d(LOG_TAG, "Key " + key + " entered the search area at [" + location.latitude + "," + location.longitude + "]");
Firebase tempRef = mFirebaseRef.child(key);
tempRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
// I add the deal only if it doesn't exist already in the adapter
String key = snapshot.getKey();
if (!mItemListAdapter.exists(key)) {
Log.d(LOG_TAG, "item added " + key);
mItemListAdapter.addSingle(snapshot);
mItemListAdapter.notifyDataSetChanged();
mListContainer.setState(MultiStateView.ContentState.CONTENT);
} else {
//...otherwise I will update the record
Log.d(LOG_TAG, "item updated: " + key);
mItemListAdapter.update(snapshot, key);
mItemListAdapter.notifyDataSetChanged();
}
}
@Override
public void onCancelled(FirebaseError firebaseError) {
Log.d(LOG_TAG, "cancelled with error:" + firebaseError.getMessage());
}
});
}
@Override
public void onKeyExited(String key) {
Log.d(LOG_TAG, "deal " + key + " is no longer in the search area");
mItemListAdapter.remove(key);
isListEmpty();
}
@Override
public void onKeyMoved(String key, GeoLocation location) {
Log.d(LOG_TAG, String.format("Key " + key + " moved within the search area to [%f,%f]", location.latitude, location.longitude));
}
@Override
public void onGeoQueryReady() {
Log.d(LOG_TAG, "All initial data has been loaded and events have been fired!");
isListEmpty();
mActiveGeoQuery = true;
}
@Override
public void onGeoQueryError(FirebaseError error) {
Log.e(LOG_TAG, "There was an error with this query: " + error);
mListContainer.setState(MultiStateView.ContentState.ERROR_GENERAL);
mListContainer.setCustomErrorString(mContext.getString(R.string.list_error_message));
mActiveGeoQuery = false;
}
});
bindListView();
}
private void isListEmpty() {
if (mItemListAdapter.getCount() == 0) {
mListContainer.setState(MultiStateView.ContentState.EMPTY);
if (mListView.hasFocus()) {
mListContainer.setEmptyString(getString(R.string.empty_list_description));
}
} else {
mListContainer.setState(MultiStateView.ContentState.CONTENT);
}
}
private void checkForItems() {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
if (mListView.hasFocus()) {
if (mItemListAdapter.getCount() == 0) {
mListContainer.setState(MultiStateView.ContentState.EMPTY);
mListContainer.setEmptyString(getString(R.string.empty_list_description));
}
}
}
}, 8000);
}
public void filterList(String query) {
if (query.equals("")) {
mItemListAdapter = new ItemListAdapter(mFirebaseRef.limitToFirst(20), getActivity(), R.layout.list_item_items);
} else {
mItemListAdapter = new ItemListAdapter(mFirebaseRef.orderByValue().startAt(query), getActivity(), R.layout.list_item_items);
}
bindListView();
}
@Override
public void onConnected(Bundle bundle) {
Log.d(LOG_TAG, "connected");
mLastLocation = LocationServices.FusedLocationApi.getLastLocation(
mGoogleApiClient);
if (mLastLocation != null) {
mLatitudeText = mLastLocation.getLatitude();
mLongitudeText = mLastLocation.getLongitude();
Log.d(LOG_TAG, "lat: " + mLatitudeText + ", long: " + mLongitudeText);
center = new GeoLocation(PrefsUtils.getPreferredLat(mContext), PrefsUtils.getPreferredLong(mContext));
if (!mActiveGeoQuery) {
PrefsUtils.setPreferredLatLong(mContext, mLatitudeText, mLongitudeText);
center = new GeoLocation(mLatitudeText, mLongitudeText);
startGeoQuery();
mItemListAdapter.notifyDataSetChanged();
}
}
}
@Override
public void onConnectionSuspended(int i) {
// The connection to Google Play services was lost for some reason. We call connect() to
// attempt to re-establish the connection.
Log.d(LOG_TAG, "Connection suspended");
mGoogleApiClient.connect();
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.d(LOG_TAG, "connection failed" + connectionResult.getErrorCode());
}
protected void createLocationRequest() {
mLocationRequest = new LocationRequest();
// Sets the desired interval for active location updates. This interval is
// inexact. You may not receive updates at all if no location sources are available, or
// you may receive them slower than requested. You may also receive updates faster than
// requested if other applications are requesting location at a faster interval.
mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
// Sets the fastest rate for active location updates. This interval is exact, and your
// application will never receive updates faster than this value.
mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}
protected void buildLocationSettingsRequest() {
LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
builder.addLocationRequest(mLocationRequest);
mLocationSettingsRequest = builder.build();
}
protected void checkLocationSettings() {
PendingResult<LocationSettingsResult> result =
LocationServices.SettingsApi.checkLocationSettings(
mGoogleApiClient,
mLocationSettingsRequest
);
result.setResultCallback(this);
}
@Override
public void onResult(LocationSettingsResult locationSettingsResult) {
final Status status = locationSettingsResult.getStatus();
switch (status.getStatusCode()) {
case LocationSettingsStatusCodes.SUCCESS:
Log.i(LOG_TAG, "All location settings are satisfied.");
break;
case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
Log.i(LOG_TAG, "Location settings are not satisfied. Show the user a dialog to" +
"upgrade location settings ");
try {
// Show the dialog by calling startResolutionForResult(), and check the result
// in onActivityResult().
status.startResolutionForResult(getActivity(), REQUEST_CHECK_SETTINGS);
} catch (IntentSender.SendIntentException e) {
Log.i(LOG_TAG, "PendingIntent unable to execute request.");
}
break;
case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
Log.i(LOG_TAG, "Location settings are inadequate, and cannot be fixed here. Dialog " +
"not created.");
break;
default:
Log.i(LOG_TAG, "Result not recognized");
}
}
}
@emanuelet
Copy link
Author

To be used in conjunction with this Adapter

@R00t6
Copy link

R00t6 commented Jul 18, 2016

I am having an issue with ItemListAdapter is abstract and cannot be instantiated.

mItemListAdapter = new ItemListAdapter(mFirebaseRef.equalTo("GeoFire"), getActivity(), R.layout.activity_post_detail);

Here is my ItemListAdapter code.

public abstract class ItemListAdapter<T> extends BaseAdapter implements Filterable {

    private static final String LOG_TAG = "FirebaseListAdapter";
    private Query mRef;
    private Class<T> mModelClass;
    private int mLayout;
    private LayoutInflater mInflater;
    private List<T> mModels;
    private List<T> mFilteredModels;
    private Map<String, T> mModelKeys;
    private Map<String, T> mFilteredKeys;
    private ChildEventListener mListener;
    private Context mContext;
    private ValueFilter valueFilter;


    /**
     * @param mRef        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 mModelClass Firebase will marshall the data at a location into an instance of a class that you provide
     * @param mLayout     This is the mLayout 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 mModelClass.
     * @param activity    The activity containing the ListView
     */
    public ItemListAdapter(Query mRef, Class<T> mModelClass, int mLayout, Activity activity) {
        this.mRef = mRef;
        this.mModelClass = mModelClass;
        this.mLayout = mLayout;
        mInflater = activity.getLayoutInflater();
        mModels = new ArrayList<>();
        mModelKeys = new HashMap<>();
        // Look for all child events. We will then map them to our own internal ArrayList, which backs ListView
        mListener = this.mRef.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {

                T model = dataSnapshot.getValue(FireBaseListAdapter.this.mModelClass);
                mModelKeys.put(dataSnapshot.getKey(), model);

                // Insert into the correct location, based on previousChildName
                if (previousChildName == null) {
                    mModels.add(0, model);
                } else {
                    T previousModel = mModelKeys.get(previousChildName);
                    int previousIndex = mModels.indexOf(previousModel);
                    int nextIndex = previousIndex + 1;
                    if (nextIndex == mModels.size()) {
                        mModels.add(model);
                    } else {
                        mModels.add(nextIndex, model);
                    }
                }

                notifyDataSetChanged();
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {
                Log.d(LOG_TAG, "onChildChanged");
                // One of the mModels changed. Replace it in our list and name mapping
                String modelName = dataSnapshot.getKey();
                T oldModel = mModelKeys.get(modelName);
                T newModel = dataSnapshot.getValue(FireBaseListAdapter.this.mModelClass);
                int index = mModels.indexOf(oldModel);

                mModels.set(index, newModel);
                mModelKeys.put(modelName, newModel);

                notifyDataSetChanged();
            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
                Log.d(LOG_TAG, "onChildRemoved");
                // A model was removed from the list. Remove it from our list and the name mapping
                String modelName = dataSnapshot.getKey();
                T oldModel = mModelKeys.get(modelName);
                mModels.remove(oldModel);
                mModelKeys.remove(modelName);
                notifyDataSetChanged();
            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
                Log.d(LOG_TAG, "onChildMoved");
                // A model changed position in the list. Update our list accordingly
                String modelName = dataSnapshot.getKey();
                T oldModel = mModelKeys.get(modelName);
                T newModel = dataSnapshot.getValue(FireBaseListAdapter.this.mModelClass);
                int index = mModels.indexOf(oldModel);
                mModels.remove(index);
                if (previousChildName == null) {
                    mModels.add(0, newModel);
                } else {
                    T previousModel = mModelKeys.get(previousChildName);
                    int previousIndex = mModels.indexOf(previousModel);
                    int nextIndex = previousIndex + 1;
                    if (nextIndex == mModels.size()) {
                        mModels.add(newModel);
                    } else {
                        mModels.add(nextIndex, newModel);
                    }
                }
                notifyDataSetChanged();
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                Log.e("FirebaseListAdapter", "Listen was cancelled, no more updates will occur");
            }
        });
    }

What is to be entered for the Model Class in the ItemListAdapter?

@R00t6
Copy link

R00t6 commented Jul 18, 2016

I am getting NullPointerException atif (!mFireAdapter.exists(key)) mFireAdapter is FireBaseListAdapter.

java.lang.NullPointerException: Attempt to invoke virtual method 'void com.android.pokechat.FireBaseListAdapter.update(com.google.firebase.database.DataSnapshot, java.lang.String)' on a null object reference
                                                                        at com.android.pokechat.fragment.GeoPostsFragment$3$1.onDataChange(GeoPostsFragment.java:236)

Here it shows the information correctly in my logcat.

07-19 01:49:26.640 7703-7703/com.android.pokechat D/GeoPostsFragment: Key -KMsopZitLFzcxFuPOCn entered the search area at [40.7853889,-122.4056973]
07-19 01:49:26.642 7703-7703/com.android.pokechat D/GeoPostsFragment: Key -KMu2IcjIyMx5Qpb5Kfh entered the search area at [40.7853889,-122.4056973]
07-19 01:49:26.642 7703-7703/com.android.pokechat D/GeoPostsFragment: Key -KMz3knxKsAAixlf4ijY entered the search area at [40.7853889,-122.4056973]
07-19 01:49:26.642 7703-7703/com.android.pokechat D/GeoPostsFragment: Key -KMzXh2Kf7nLdOPqnMpY entered the search area at [40.7853889,-122.4056973]

Is this is where I could be possibly wrong I am using your ExampleAdapter as mItemListAdapter

mItemListAdapter = new ItemListAdapter(mFirebaseRef.equalTo("GeoFire"),getActivity(), R.layout.activity_post_detail);

Please help me resolve the issue I am quite ahead with my project finishing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment