package io.fullstack.firestack; import android.content.Context; import android.util.Log; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import android.net.Uri; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReactContext; import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.OnDisconnect; import com.google.firebase.database.Query; import com.google.firebase.database.ValueEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; class FirestackDBReference { private static final String TAG = "FirestackDBReference"; private String mPath; private ReadableArray mModifiers; private HashMap<String, Boolean> mListeners = new HashMap<String, Boolean>(); private FirestackDatabaseModule mDatabase; private ChildEventListener mEventListener; private ValueEventListener mValueListener; private ValueEventListener mOnceValueListener; private ReactContext mReactContext; public FirestackDBReference(final ReactContext context, final String path) { mReactContext = context; mPath = path; } public void setModifiers(final ReadableArray modifiers) { mModifiers = modifiers; } public void addChildEventListener(final String name, final ReadableArray modifiers) { final FirestackDBReference self = this; if (mEventListener == null) { mEventListener = new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { self.handleDatabaseEvent("child_added", mPath, dataSnapshot); } @Override public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { self.handleDatabaseEvent("child_changed", mPath, dataSnapshot); } @Override public void onChildRemoved(DataSnapshot dataSnapshot) { self.handleDatabaseEvent("child_removed", mPath, dataSnapshot); } @Override public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { self.handleDatabaseEvent("child_moved", mPath, dataSnapshot); } @Override public void onCancelled(DatabaseError error) { self.handleDatabaseError(name, mPath, error); } }; } Query ref = this.getDatabaseQueryAtPathAndModifiers(modifiers); ref.addChildEventListener(mEventListener); this.setListeningTo(mPath, name); } public void addValueEventListener(final String name, final ReadableArray modifiers) { final FirestackDBReference self = this; mValueListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { self.handleDatabaseEvent("value", mPath, dataSnapshot); } @Override public void onCancelled(DatabaseError error) { self.handleDatabaseError("value", mPath, error); } }; Query ref = this.getDatabaseQueryAtPathAndModifiers(modifiers); ref.addValueEventListener(mValueListener); this.setListeningTo(mPath, "value"); } public void addOnceValueEventListener(final ReadableArray modifiers, final Callback callback) { final FirestackDBReference self = this; mOnceValueListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { WritableMap data = FirestackUtils.dataSnapshotToMap("value", mPath, dataSnapshot); callback.invoke(null, data); } @Override public void onCancelled(DatabaseError error) { WritableMap err = Arguments.createMap(); err.putInt("errorCode", error.getCode()); err.putString("errorDetails", error.getDetails()); err.putString("description", error.getMessage()); callback.invoke(err); } }; Query ref = this.getDatabaseQueryAtPathAndModifiers(modifiers); ref.addListenerForSingleValueEvent(mOnceValueListener); } public Boolean isListeningTo(final String path, final String evtName) { String key = this.pathListeningKey(path, evtName); return mListeners.containsKey(key); } /** * Note: these path/eventType listeners only get removed when javascript calls .off() and cleanup is run on the entire path */ public void setListeningTo(final String path, final String evtName) { String key = this.pathListeningKey(path, evtName); mListeners.put(key, true); } public void notListeningTo(final String path, final String evtName) { String key = this.pathListeningKey(path, evtName); mListeners.remove(key); } private String pathListeningKey(final String path, final String eventName) { return "listener/" + path + "/" + eventName; } public void cleanup() { Log.d(TAG, "cleaning up database reference " + this); this.removeChildEventListener(); this.removeValueEventListener(); } public void removeChildEventListener() { if (mEventListener != null) { DatabaseReference ref = this.getDatabaseRef(); ref.removeEventListener(mEventListener); this.notListeningTo(mPath, "child_added"); this.notListeningTo(mPath, "child_changed"); this.notListeningTo(mPath, "child_removed"); this.notListeningTo(mPath, "child_moved"); mEventListener = null; } } public void removeValueEventListener() { DatabaseReference ref = this.getDatabaseRef(); if (mValueListener != null) { ref.removeEventListener(mValueListener); this.notListeningTo(mPath, "value"); mValueListener = null; } if (mOnceValueListener != null) { ref.removeEventListener(mOnceValueListener); mOnceValueListener = null; } } private void handleDatabaseEvent(final String name, final String path, final DataSnapshot dataSnapshot) { if (!FirestackDBReference.this.isListeningTo(path, name)) { return; } WritableMap data = FirestackUtils.dataSnapshotToMap(name, path, dataSnapshot); WritableMap evt = Arguments.createMap(); evt.putString("eventName", name); evt.putString("path", path); evt.putMap("body", data); FirestackUtils.sendEvent(mReactContext, "database_event", evt); } private void handleDatabaseError(final String name, final String path, final DatabaseError error) { WritableMap err = Arguments.createMap(); err.putInt("errorCode", error.getCode()); err.putString("errorDetails", error.getDetails()); err.putString("description", error.getMessage()); WritableMap evt = Arguments.createMap(); evt.putString("eventName", name); evt.putString("path", path); evt.putMap("body", err); FirestackUtils.sendEvent(mReactContext, "database_error", evt); } public DatabaseReference getDatabaseRef() { return FirebaseDatabase.getInstance().getReference(mPath); } private Query getDatabaseQueryAtPathAndModifiers(final ReadableArray modifiers) { DatabaseReference ref = this.getDatabaseRef(); List<Object> strModifiers = FirestackUtils.recursivelyDeconstructReadableArray(modifiers); ListIterator<Object> it = strModifiers.listIterator(); Query query = ref.orderByKey(); while(it.hasNext()) { String str = (String) it.next(); String[] strArr = str.split(":"); String methStr = strArr[0]; if (methStr.equalsIgnoreCase("orderByKey")) { query = ref.orderByKey(); } else if (methStr.equalsIgnoreCase("orderByValue")) { query = ref.orderByValue(); } else if (methStr.equalsIgnoreCase("orderByPriority")) { query = ref.orderByPriority(); } else if (methStr.contains("orderByChild")) { String key = strArr[1]; Log.d(TAG, "orderByChild: " + key); query = ref.orderByChild(key); } else if (methStr.contains("limitToLast")) { String key = strArr[1]; int limit = Integer.parseInt(key); Log.d(TAG, "limitToLast: " + limit); query = query.limitToLast(limit); } else if (methStr.contains("limitToFirst")) { String key = strArr[1]; int limit = Integer.parseInt(key); Log.d(TAG, "limitToFirst: " + limit); query = query.limitToFirst(limit); } else if (methStr.contains("equalTo")) { String value = strArr[1]; String key = strArr.length >= 3 ? strArr[2] : null; if (key == null) { query = query.equalTo(value); } else { query = query.equalTo(value, key); } } else if (methStr.contains("endAt")) { String value = strArr[1]; String key = strArr.length >= 3 ? strArr[2] : null; if (key == null) { query = query.endAt(value); } else { query = query.endAt(value, key); } } else if (methStr.contains("startAt")) { String value = strArr[1]; String key = strArr.length >= 3 ? strArr[2] : null; if (key == null) { query = query.startAt(value); } else { query = query.startAt(value, key); } } } return query; } } class FirestackDatabaseModule extends ReactContextBaseJavaModule { private static final String TAG = "FirestackDatabase"; private Context context; private ReactContext mReactContext; private HashMap<String, FirestackDBReference> mDBListeners = new HashMap<String, FirestackDBReference>(); public FirestackDatabaseModule(ReactApplicationContext reactContext) { super(reactContext); this.context = reactContext; mReactContext = reactContext; } @Override public String getName() { return TAG; } // Persistence @ReactMethod public void enablePersistence( final Boolean enable, final Callback callback) { try { FirebaseDatabase.getInstance() .setPersistenceEnabled(enable); } catch (Throwable t) { Log.e(TAG, "FirebaseDatabase setPersistenceEnabled exception", t); } WritableMap res = Arguments.createMap(); res.putString("status", "success"); callback.invoke(null, res); } @ReactMethod public void keepSynced( final String path, final Boolean enable, final Callback callback) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); ref.keepSynced(enable); WritableMap res = Arguments.createMap(); res.putString("status", "success"); res.putString("path", path); callback.invoke(null, res); } // Database @ReactMethod public void set( final String path, final ReadableMap props, final Callback callback) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); final FirestackDatabaseModule self = this; Map<String, Object> m = FirestackUtils.recursivelyDeconstructReadableMap(props); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { handleCallback("set", callback, error, ref); } }; ref.setValue(m, listener); } @ReactMethod public void update(final String path, final ReadableMap props, final Callback callback) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); final FirestackDatabaseModule self = this; Map<String, Object> m = FirestackUtils.recursivelyDeconstructReadableMap(props); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { handleCallback("update", callback, error, ref); } }; ref.updateChildren(m, listener); } @ReactMethod public void remove(final String path, final Callback callback) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); final FirestackDatabaseModule self = this; DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { handleCallback("remove", callback, error, ref); } }; ref.removeValue(listener); } @ReactMethod public void push(final String path, final ReadableMap props, final Callback callback) { Log.d(TAG, "Called push with " + path); DatabaseReference ref = this.getDatabaseReferenceAtPath(path); DatabaseReference newRef = ref.push(); final Uri url = Uri.parse(newRef.toString()); final String newPath = url.getPath(); ReadableMapKeySetIterator iterator = props.keySetIterator(); if (iterator.hasNextKey()) { Log.d(TAG, "Passed value to push"); // lame way to check if the `props` are empty final FirestackDatabaseModule self = this; Map<String, Object> m = FirestackUtils.recursivelyDeconstructReadableMap(props); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { if (error != null) { WritableMap err = Arguments.createMap(); err.putInt("errorCode", error.getCode()); err.putString("errorDetails", error.getDetails()); err.putString("description", error.getMessage()); callback.invoke(err); } else { WritableMap res = Arguments.createMap(); res.putString("status", "success"); res.putString("ref", newPath); callback.invoke(null, res); } } }; newRef.setValue(m, listener); } else { Log.d(TAG, "No value passed to push: " + newPath); WritableMap res = Arguments.createMap(); res.putString("result", "success"); res.putString("ref", newPath); callback.invoke(null, res); } } @ReactMethod public void on(final String path, final ReadableArray modifiers, final String name, final Callback callback) { FirestackDBReference ref = this.getDBHandle(path); WritableMap resp = Arguments.createMap(); if (name.equals("value")) { ref.addValueEventListener(name, modifiers); } else { ref.addChildEventListener(name, modifiers); } this.saveDBHandle(path, ref); resp.putString("result", "success"); Log.d(TAG, "Added listener " + name + " for " + ref); resp.putString("handle", path); callback.invoke(null, resp); } @ReactMethod public void onOnce(final String path, final ReadableArray modifiers, final String name, final Callback callback) { Log.d(TAG, "Setting one-time listener on event: " + name + " for path " + path); FirestackDBReference ref = this.getDBHandle(path); ref.addOnceValueEventListener(modifiers, callback); } /** * At the time of this writing, off() only gets called when there are no more subscribers to a given path. * `mListeners` might therefore be out of sync (though javascript isnt listening for those eventTypes, so * it doesn't really matter- just polluting the RN bridge a little more than necessary. * off() should therefore clean *everything* up */ @ReactMethod public void off(final String path, @Deprecated final String name, final Callback callback) { this.removeDBHandle(path); Log.d(TAG, "Removed listener " + path); WritableMap resp = Arguments.createMap(); resp.putString("handle", path); resp.putString("result", "success"); callback.invoke(null, resp); } // On Disconnect @ReactMethod public void onDisconnectSetObject(final String path, final ReadableMap props, final Callback callback) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); Map<String, Object> m = FirestackUtils.recursivelyDeconstructReadableMap(props); OnDisconnect od = ref.onDisconnect(); od.setValue(m, new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { handleCallback("onDisconnectSetObject", callback, databaseError, databaseReference); } }); } @ReactMethod public void onDisconnectSetString(final String path, final String value, final Callback callback) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); OnDisconnect od = ref.onDisconnect(); od.setValue(value, new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { handleCallback("onDisconnectSetString", callback, databaseError, databaseReference); } }); } @ReactMethod public void onDisconnectRemove(final String path, final Callback callback) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); OnDisconnect od = ref.onDisconnect(); od.removeValue(new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { handleCallback("onDisconnectRemove", callback, databaseError, databaseReference); } }); } @ReactMethod public void onDisconnectCancel(final String path, final Callback callback) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); OnDisconnect od = ref.onDisconnect(); od.cancel(new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { handleCallback("onDisconnectCancel", callback, databaseError, databaseReference); } }); } // Private helpers // private void handleDatabaseEvent(final String name, final DataSnapshot dataSnapshot) { // WritableMap data = this.dataSnapshotToMap(name, dataSnapshot); // WritableMap evt = Arguments.createMap(); // evt.putString("eventName", name); // evt.putMap("body", data); // FirestackUtils.sendEvent(mReactContext, "database_event", evt); // } // private void handleDatabaseError(final String name, final DatabaseError error) { // WritableMap err = Arguments.createMap(); // err.putInt("errorCode", error.getCode()); // err.putString("errorDetails", error.getDetails()); // err.putString("description", error.getMessage()); // WritableMap evt = Arguments.createMap(); // evt.putString("eventName", name); // evt.putMap("body", err); // FirestackUtils.sendEvent(mReactContext, "database_error", evt); // } private void handleCallback( final String methodName, final Callback callback, final DatabaseError databaseError, final DatabaseReference databaseReference) { if (databaseError != null) { WritableMap err = Arguments.createMap(); err.putInt("errorCode", databaseError.getCode()); err.putString("errorDetails", databaseError.getDetails()); err.putString("description", databaseError.getMessage()); callback.invoke(err); } else { WritableMap res = Arguments.createMap(); res.putString("status", "success"); res.putString("method", methodName); callback.invoke(null, res); } } private FirestackDBReference getDBHandle(final String path) { if (!mDBListeners.containsKey(path)) { ReactContext ctx = getReactApplicationContext(); mDBListeners.put(path, new FirestackDBReference(ctx, path)); } return mDBListeners.get(path); } private void saveDBHandle(final String path, final FirestackDBReference dbRef) { mDBListeners.put(path, dbRef); } private void removeDBHandle(final String path) { if (mDBListeners.containsKey(path)) { FirestackDBReference r = mDBListeners.get(path); r.cleanup(); mDBListeners.remove(path); } } private String keyPath(final String path, final String eventName) { return path + "-" + eventName; } // TODO: move to FirestackDBReference? private DatabaseReference getDatabaseReferenceAtPath(final String path) { DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference(path); return mDatabase; } private Query getDatabaseQueryAtPathAndModifiers( final String path, final ReadableArray modifiers) { DatabaseReference ref = this.getDatabaseReferenceAtPath(path); List<Object> strModifiers = FirestackUtils.recursivelyDeconstructReadableArray(modifiers); ListIterator<Object> it = strModifiers.listIterator(); Query query = ref.orderByKey(); while(it.hasNext()) { String str = (String) it.next(); String[] strArr = str.split(":"); String methStr = strArr[0]; if (methStr.equalsIgnoreCase("orderByKey")) { query = ref.orderByKey(); } else if (methStr.equalsIgnoreCase("orderByValue")) { query = ref.orderByValue(); } else if (methStr.equalsIgnoreCase("orderByPriority")) { query = ref.orderByPriority(); } else if (methStr.contains("orderByChild")) { String key = strArr[1]; Log.d(TAG, "orderByChild: " + key); query = ref.orderByChild(key); } else if (methStr.contains("limitToLast")) { String key = strArr[1]; int limit = Integer.parseInt(key); Log.d(TAG, "limitToLast: " + limit); query = query.limitToLast(limit); } else if (methStr.contains("limitToFirst")) { String key = strArr[1]; int limit = Integer.parseInt(key); Log.d(TAG, "limitToFirst: " + limit); query = query.limitToFirst(limit); } else if (methStr.contains("equalTo")) { String value = strArr[1]; String key = strArr.length >= 3 ? strArr[2] : null; if (key == null) { query = query.equalTo(value); } else { query = query.equalTo(value, key); } } else if (methStr.contains("endAt")) { String value = strArr[1]; String key = strArr.length >= 3 ? strArr[2] : null; if (key == null) { query = query.endAt(value); } else { query = query.endAt(value, key); } } else if (methStr.contains("startAt")) { String value = strArr[1]; String key = strArr.length >= 3 ? strArr[2] : null; if (key == null) { query = query.startAt(value); } else { query = query.startAt(value, key); } } } return query; } private WritableMap dataSnapshotToMap(String name, String path, DataSnapshot dataSnapshot) { return FirestackUtils.dataSnapshotToMap(name, path, dataSnapshot); } private <Any> Any castSnapshotValue(DataSnapshot snapshot) { if (snapshot.hasChildren()) { WritableMap data = Arguments.createMap(); for (DataSnapshot child : snapshot.getChildren()) { Any castedChild = castSnapshotValue(child); switch (castedChild.getClass().getName()) { case "java.lang.Boolean": data.putBoolean(child.getKey(), (Boolean) castedChild); break; case "java.lang.Long": data.putDouble(child.getKey(), (Long) castedChild); break; case "java.lang.Double": data.putDouble(child.getKey(), (Double) castedChild); break; case "java.lang.String": data.putString(child.getKey(), (String) castedChild); break; case "com.facebook.react.bridge.WritableNativeMap": data.putMap(child.getKey(), (WritableMap) castedChild); break; } } return (Any) data; } else { if (snapshot.getValue() != null) { String type = snapshot.getValue().getClass().getName(); switch (type) { case "java.lang.Boolean": return (Any)((Boolean) snapshot.getValue()); case "java.lang.Long": return (Any) ((Long) snapshot.getValue()); case "java.lang.Double": return (Any)((Double) snapshot.getValue()); case "java.lang.String": return (Any)((String) snapshot.getValue()); default: return (Any) null; } } else { return (Any) null; } } } }