diff --git a/android/.editorconfig b/android/.editorconfig new file mode 100644 index 00000000..0f099897 --- /dev/null +++ b/android/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..760a0a03 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,56 @@ +// START - required to allow working on this project inside Android Studio +// YES, jcenter is required twice - it somehow tricks studio into compiling deps below +// doesn't break anything anywhere else and projects using this lib work as normal +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.1.3' + } +} +// END + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + multiDexEnabled true + } + buildTypes { + release { + minifyEnabled false + } + } +} + +// START - required to allow working on this project inside Android Studio +// YES, jcenter is required twice - it somehow tricks studio into compiling deps below +// doesn't break anything anywhere else and projects using this lib work as normal +// you'll now have code completion/validation and all the other AS goodies. +allprojects { + repositories { + jcenter() + } +} +// END + +dependencies { + compile 'com.facebook.react:react-native:0.20.+' + compile 'com.google.android.gms:play-services-base:9.8.0' + compile 'com.google.firebase:firebase-core:9.8.0' + compile 'com.google.firebase:firebase-config:9.8.0' + compile 'com.google.firebase:firebase-auth:9.8.0' + compile 'com.google.firebase:firebase-analytics:9.8.0' + compile 'com.google.firebase:firebase-database:9.8.0' + compile 'com.google.firebase:firebase-storage:9.8.0' + compile 'com.google.firebase:firebase-messaging:9.8.0' +} + diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..45a2feb8 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/java/io/invertase/firebase/RNFirebaseInstanceIdService.java b/android/src/main/java/io/invertase/firebase/RNFirebaseInstanceIdService.java new file mode 100644 index 00000000..1e852635 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/RNFirebaseInstanceIdService.java @@ -0,0 +1,29 @@ +package io.invertase.firebase; + +import android.util.Log; +import android.os.Bundle; +import android.content.Intent; + +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.FirebaseInstanceIdService; + +import io.invertase.firebase.messaging.RNFirebaseMessaging; + +public class RNFirebaseInstanceIdService extends FirebaseInstanceIdService { + + private static final String TAG = "FSInstanceIdService"; + + /** + * + */ + @Override + public void onTokenRefresh() { + String refreshedToken = FirebaseInstanceId.getInstance().getToken(); + Log.d(TAG, "Refreshed token: " + refreshedToken); + Intent i = new Intent(RNFirebaseMessaging.INTENT_NAME_TOKEN); + Bundle bundle = new Bundle(); + bundle.putString("token", refreshedToken); + i.putExtras(bundle); + sendBroadcast(i); + } +} diff --git a/android/src/main/java/io/invertase/firebase/RNFirebaseMessagingService.java b/android/src/main/java/io/invertase/firebase/RNFirebaseMessagingService.java new file mode 100644 index 00000000..fedc2212 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/RNFirebaseMessagingService.java @@ -0,0 +1,74 @@ +package io.invertase.firebase; + +import android.content.Intent; +import android.util.Log; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; +import com.google.firebase.messaging.SendException; + +import io.invertase.firebase.messaging.RNFirebaseMessaging; + +public class RNFirebaseMessagingService extends FirebaseMessagingService { + + private static final String TAG = "FSMessagingService"; + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + Log.d(TAG, "Remote message received"); + // debug + Log.d(TAG, "From: " + remoteMessage.getFrom()); + + if (remoteMessage.getData().size() > 0) { + Log.d(TAG, "Message data payload: " + remoteMessage.getData()); + } + + if (remoteMessage.getNotification() != null) { + Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody()); + } + + Intent i = new Intent(RNFirebaseMessaging.INTENT_NAME_NOTIFICATION); + i.putExtra("data", remoteMessage); + sendOrderedBroadcast(i, null); + + } + + @Override + public void onMessageSent(String msgId) { + // Called when an upstream message has been successfully sent to the GCM connection server. + Log.d(TAG, "upstream message has been successfully sent"); + Intent i = new Intent(RNFirebaseMessaging.INTENT_NAME_SEND); + i.putExtra("msgId", msgId); + sendOrderedBroadcast(i, null); + } + + @Override + public void onSendError(String msgId, Exception exception) { + // Called when there was an error sending an upstream message. + Log.d(TAG, "error sending an upstream message"); + Intent i = new Intent(RNFirebaseMessaging.INTENT_NAME_SEND); + i.putExtra("msgId", msgId); + i.putExtra("hasError", true); + SendException sendException = (SendException) exception; + i.putExtra("errorCode", sendException.getErrorCode()); + switch(sendException.getErrorCode()){ + case SendException.ERROR_INVALID_PARAMETERS: + i.putExtra("errorMessage", "Message was sent with invalid parameters."); + break; + case SendException.ERROR_SIZE: + i.putExtra("errorMessage", "Message exceeded the maximum payload size."); + break; + case SendException.ERROR_TOO_MANY_MESSAGES: + i.putExtra("errorMessage", "App has too many pending messages so this one was dropped."); + break; + case SendException.ERROR_TTL_EXCEEDED: + i.putExtra("errorMessage", "Message time to live (TTL) was exceeded before the message could be sent."); + break; + case SendException.ERROR_UNKNOWN: + default: + i.putExtra("errorMessage", "Unknown error."); + break; + } + sendOrderedBroadcast(i, null); + } +} diff --git a/android/src/main/java/io/invertase/firebase/RNFirebaseModule.java b/android/src/main/java/io/invertase/firebase/RNFirebaseModule.java new file mode 100644 index 00000000..17c1ec39 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/RNFirebaseModule.java @@ -0,0 +1,208 @@ +package io.invertase.firebase; + +import java.util.Map; +import java.util.HashMap; + +import android.util.Log; +import android.content.Context; +import android.support.annotation.Nullable; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.database.ServerValue; + +interface KeySetterFn { + String setKeyOrDefault(String a, String b); +} + +@SuppressWarnings("WeakerAccess") +public class RNFirebaseModule extends ReactContextBaseJavaModule implements LifecycleEventListener { + private static final String TAG = "RNFirebase"; + private FirebaseApp app; + + public RNFirebaseModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return TAG; + } + + private WritableMap getPlayServicesStatus() { + GoogleApiAvailability gapi = GoogleApiAvailability.getInstance(); + final int status = gapi.isGooglePlayServicesAvailable(getReactApplicationContext()); + WritableMap result = Arguments.createMap(); + result.putInt("status", status); + if (status == ConnectionResult.SUCCESS) { + result.putBoolean("isAvailable", true); + } else { + result.putBoolean("isAvailable", false); + result.putBoolean("isUserResolvableError", gapi.isUserResolvableError(status)); + result.putString("error", gapi.getErrorString(status)); + } + return result; + } + + @ReactMethod + public void configureWithOptions(final ReadableMap params, @Nullable final Callback onComplete) { + Log.i(TAG, "configureWithOptions"); + + FirebaseOptions.Builder builder = new FirebaseOptions.Builder(); + FirebaseOptions defaultOptions = FirebaseOptions.fromResource(getReactApplicationContext().getBaseContext()); + + if (defaultOptions == null) { + defaultOptions = new FirebaseOptions.Builder().build(); + } + + KeySetterFn fn = new KeySetterFn() { + public String setKeyOrDefault( + final String key, + final String defaultValue) { + if (params.hasKey(key)) { + // User-set key + final String val = params.getString(key); + Log.d(TAG, "Setting " + key + " from params to: " + val); + return val; + } else if (defaultValue != null && !defaultValue.equals("")) { + Log.d(TAG, "Setting " + key + " from params to: " + defaultValue); + return defaultValue; + } else { + return null; + } + } + }; + + String val = fn.setKeyOrDefault("applicationId", defaultOptions.getApplicationId()); + if (val != null) builder.setApplicationId(val); + + val = fn.setKeyOrDefault("apiKey", defaultOptions.getApiKey()); + if (val != null) builder.setApiKey(val); + + val = fn.setKeyOrDefault("gcmSenderID", defaultOptions.getGcmSenderId()); + if (val != null) builder.setGcmSenderId(val); + + val = fn.setKeyOrDefault("storageBucket", defaultOptions.getStorageBucket()); + if (val != null) builder.setStorageBucket(val); + + val = fn.setKeyOrDefault("databaseURL", defaultOptions.getDatabaseUrl()); + if (val != null) builder.setDatabaseUrl(val); + + val = fn.setKeyOrDefault("databaseUrl", defaultOptions.getDatabaseUrl()); + if (val != null) builder.setDatabaseUrl(val); + + val = fn.setKeyOrDefault("clientId", defaultOptions.getApplicationId()); + if (val != null) builder.setApplicationId(val); + + + // if (params.hasKey("applicationId")) { + // final String applicationId = params.getString("applicationId"); + // Log.d(TAG, "Setting applicationId from params " + applicationId); + // builder.setApplicationId(applicationId); + // } + // if (params.hasKey("apiKey")) { + // final String apiKey = params.getString("apiKey"); + // Log.d(TAG, "Setting API key from params " + apiKey); + // builder.setApiKey(apiKey); + // } + // if (params.hasKey("APIKey")) { + // final String apiKey = params.getString("APIKey"); + // Log.d(TAG, "Setting API key from params " + apiKey); + // builder.setApiKey(apiKey); + // } + // if (params.hasKey("gcmSenderID")) { + // final String gcmSenderID = params.getString("gcmSenderID"); + // Log.d(TAG, "Setting gcmSenderID from params " + gcmSenderID ); + // builder.setGcmSenderId(gcmSenderID); + // } + // if (params.hasKey("storageBucket")) { + // final String storageBucket = params.getString("storageBucket"); + // Log.d(TAG, "Setting storageBucket from params " + storageBucket); + // builder.setStorageBucket(storageBucket); + // } + // if (params.hasKey("databaseURL")) { + // final String databaseURL = params.getString("databaseURL"); + // Log.d(TAG, "Setting databaseURL from params " + databaseURL); + // builder.setDatabaseUrl(databaseURL); + // } + // if (params.hasKey("clientID")) { + // final String clientID = params.getString("clientID"); + // Log.d(TAG, "Setting clientID from params " + clientID); + // builder.setApplicationId(clientID); + // } + + try { + Log.i(TAG, "Configuring app"); + if (app == null) { + app = FirebaseApp.initializeApp(getReactApplicationContext().getBaseContext(), builder.build()); + } + Log.i(TAG, "Configured"); + + WritableMap resp = Arguments.createMap(); + resp.putString("msg", "success"); + onComplete.invoke(null, resp); + } catch (Exception ex) { + Log.e(TAG, "ERROR configureWithOptions"); + Log.e(TAG, ex.getMessage()); + + WritableMap resp = Arguments.createMap(); + resp.putString("msg", ex.getMessage()); + + onComplete.invoke(resp); + } + } + + @ReactMethod + public void serverValue(@Nullable final Callback onComplete) { + WritableMap timestampMap = Arguments.createMap(); + for (Map.Entry entry : ServerValue.TIMESTAMP.entrySet()) { + timestampMap.putString(entry.getKey(), entry.getValue()); + } + + WritableMap map = Arguments.createMap(); + map.putMap("TIMESTAMP", timestampMap); + if (onComplete != null) onComplete.invoke(null, map); + } + + // Internal helpers + @Override + public void onHostResume() { + WritableMap params = Arguments.createMap(); + params.putBoolean("isForground", true); + Utils.sendEvent(getReactApplicationContext(), "RNFirebaseAppState", params); + } + + @Override + public void onHostPause() { + WritableMap params = Arguments.createMap(); + params.putBoolean("isForground", false); + Utils.sendEvent(getReactApplicationContext(), "RNFirebaseAppState", params); + } + + @Override + public void onHostDestroy() { + + } + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + constants.put("googleApiAvailability", getPlayServicesStatus()); + + // TODO remove once this has been moved on ios + constants.put("serverValueTimestamp", ServerValue.TIMESTAMP); + return constants; + } +} diff --git a/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java new file mode 100644 index 00000000..e8bd891e --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java @@ -0,0 +1,64 @@ +package io.invertase.firebase; + +import android.content.Context; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; + +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +import io.invertase.firebase.auth.RNFirebaseAuth; +import io.invertase.firebase.storage.RNFirebaseStorage; +import io.invertase.firebase.database.RNFirebaseDatabase; +import io.invertase.firebase.analytics.RNFirebaseAnalytics; +import io.invertase.firebase.messaging.RNFirebaseMessaging; + +@SuppressWarnings("unused") +public class RNFirebasePackage implements ReactPackage { + private Context mContext; + + public RNFirebasePackage() { + } + /** + * @param reactContext react application context that can be used to create modules + * @return list of native modules to register with the newly created catalyst instance + */ + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new RNFirebaseModule(reactContext)); + modules.add(new RNFirebaseAuth(reactContext)); + modules.add(new RNFirebaseDatabase(reactContext)); + modules.add(new RNFirebaseAnalytics(reactContext)); + modules.add(new RNFirebaseStorage(reactContext)); + modules.add(new RNFirebaseMessaging(reactContext)); + return modules; + } + + /** + * @return list of JS modules to register with the newly created catalyst instance. + *

+ * IMPORTANT: Note that only modules that needs to be accessible from the native code should be + * listed here. Also listing a native module here doesn't imply that the JS implementation of it + * will be automatically included in the JS bundle. + */ + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + /** + * @param reactContext + * @return a list of view managers that should be registered with {@link UIManagerModule} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/android/src/main/java/io/invertase/firebase/Utils.java b/android/src/main/java/io/invertase/firebase/Utils.java new file mode 100644 index 00000000..e836e4e7 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/Utils.java @@ -0,0 +1,301 @@ +package io.invertase.firebase; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.ReadableArray; +import com.google.firebase.database.DataSnapshot; +import com.facebook.react.bridge.ReadableMapKeySetIterator; + +@SuppressWarnings("WeakerAccess") +public class Utils { + private static final String TAG = "Utils"; + + // TODO NOTE + public static void todoNote(final String tag, final String name, final Callback callback) { + Log.e(tag, "The method " + name + " has not yet been implemented."); + Log.e(tag, "Feel free to contribute to finish the method in the source."); + + WritableMap errorMap = Arguments.createMap(); + errorMap.putString("error", "unimplemented"); + callback.invoke(null, errorMap); + } + + /** + * send a JS event + **/ + public static void sendEvent(final ReactContext context, final String eventName, final WritableMap params) { + if (context != null) { + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); + } else { + Log.d(TAG, "Missing context - cannot send event!"); + } + } + + // snapshot + public static WritableMap dataSnapshotToMap( + String name, + String path, + String modifiersString, + DataSnapshot dataSnapshot + ) { + WritableMap data = Arguments.createMap(); + + data.putString("key", dataSnapshot.getKey()); + data.putBoolean("exists", dataSnapshot.exists()); + data.putBoolean("hasChildren", dataSnapshot.hasChildren()); + + data.putDouble("childrenCount", dataSnapshot.getChildrenCount()); + if (!dataSnapshot.hasChildren()) { + Object value = dataSnapshot.getValue(); + String type = value != null ? value.getClass().getName() : ""; + switch (type) { + case "java.lang.Boolean": + data.putBoolean("value", (Boolean) value); + break; + case "java.lang.Long": + Long longVal = (Long) value; + data.putDouble("value", (double) longVal); + break; + case "java.lang.Double": + data.putDouble("value", (Double) value); + break; + case "java.lang.String": + data.putString("value", (String) value); + break; + default: + data.putString("value", null); + } + } else { + Object value = Utils.castSnapshotValue(dataSnapshot); + if (value instanceof WritableNativeArray) { + data.putArray("value", (WritableArray) value); + } else { + data.putMap("value", (WritableMap) value); + } + } + + // Child keys + WritableArray childKeys = Utils.getChildKeys(dataSnapshot); + data.putArray("childKeys", childKeys); + + Object priority = dataSnapshot.getPriority(); + if (priority == null) { + data.putString("priority", null); + } else { + data.putString("priority", priority.toString()); + } + + WritableMap eventMap = Arguments.createMap(); + eventMap.putString("eventName", name); + eventMap.putMap("snapshot", data); + eventMap.putString("path", path); + eventMap.putString("modifiersString", modifiersString); + return eventMap; + } + + public static Any castSnapshotValue(DataSnapshot snapshot) { + if (snapshot.hasChildren()) { + if (isArray(snapshot)) { + return (Any) buildArray(snapshot); + } else { + return (Any) buildMap(snapshot); + } + } else { + if (snapshot.getValue() != null) { + String type = snapshot.getValue().getClass().getName(); + switch (type) { + case "java.lang.Boolean": + return (Any) (snapshot.getValue()); + case "java.lang.Long": + return (Any) (snapshot.getValue()); + case "java.lang.Double": + return (Any) (snapshot.getValue()); + case "java.lang.String": + return (Any) (snapshot.getValue()); + default: + Log.w(TAG, "Invalid type: " + type); + return (Any) null; + } + } + return (Any) null; + } + } + + private static boolean isArray(DataSnapshot snapshot) { + long expectedKey = 0; + for (DataSnapshot child : snapshot.getChildren()) { + try { + long key = Long.parseLong(child.getKey()); + if (key == expectedKey) { + expectedKey++; + } else { + return false; + } + } catch (NumberFormatException ex) { + return false; + } + } + return true; + } + + private static WritableArray buildArray(DataSnapshot snapshot) { + WritableArray array = Arguments.createArray(); + for (DataSnapshot child : snapshot.getChildren()) { + Any castedChild = castSnapshotValue(child); + switch (castedChild.getClass().getName()) { + case "java.lang.Boolean": + array.pushBoolean((Boolean) castedChild); + break; + case "java.lang.Long": + Long longVal = (Long) castedChild; + array.pushDouble((double) longVal); + break; + case "java.lang.Double": + array.pushDouble((Double) castedChild); + break; + case "java.lang.String": + array.pushString((String) castedChild); + break; + case "com.facebook.react.bridge.WritableNativeMap": + array.pushMap((WritableMap) castedChild); + break; + case "com.facebook.react.bridge.WritableNativeArray": + array.pushArray((WritableArray) castedChild); + break; + default: + Log.w(TAG, "Invalid type: " + castedChild.getClass().getName()); + break; + } + } + return array; + } + + private static WritableMap buildMap(DataSnapshot snapshot) { + WritableMap map = Arguments.createMap(); + for (DataSnapshot child : snapshot.getChildren()) { + Any castedChild = castSnapshotValue(child); + + switch (castedChild.getClass().getName()) { + case "java.lang.Boolean": + map.putBoolean(child.getKey(), (Boolean) castedChild); + break; + case "java.lang.Long": + Long longVal = (Long) castedChild; + map.putDouble(child.getKey(), (double) longVal); + break; + case "java.lang.Double": + map.putDouble(child.getKey(), (Double) castedChild); + break; + case "java.lang.String": + map.putString(child.getKey(), (String) castedChild); + break; + case "com.facebook.react.bridge.WritableNativeMap": + map.putMap(child.getKey(), (WritableMap) castedChild); + break; + case "com.facebook.react.bridge.WritableNativeArray": + map.putArray(child.getKey(), (WritableArray) castedChild); + break; + default: + Log.w(TAG, "Invalid type: " + castedChild.getClass().getName()); + break; + } + } + return map; + } + + public static WritableArray getChildKeys(DataSnapshot snapshot) { + WritableArray childKeys = Arguments.createArray(); + + if (snapshot.hasChildren()) { + for (DataSnapshot child : snapshot.getChildren()) { + childKeys.pushString(child.getKey()); + } + } + + return childKeys; + } + + public static Map recursivelyDeconstructReadableMap(ReadableMap readableMap) { + Map deconstructedMap = new HashMap<>(); + if (readableMap == null) { + return deconstructedMap; + } + + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = readableMap.getType(key); + switch (type) { + case Null: + deconstructedMap.put(key, null); + break; + case Boolean: + deconstructedMap.put(key, readableMap.getBoolean(key)); + break; + case Number: + deconstructedMap.put(key, readableMap.getDouble(key)); + break; + case String: + deconstructedMap.put(key, readableMap.getString(key)); + break; + case Map: + deconstructedMap.put(key, Utils.recursivelyDeconstructReadableMap(readableMap.getMap(key))); + break; + case Array: + deconstructedMap.put(key, Utils.recursivelyDeconstructReadableArray(readableMap.getArray(key))); + break; + default: + throw new IllegalArgumentException("Could not convert object with key: " + key + "."); + } + + } + return deconstructedMap; + } + + public static List recursivelyDeconstructReadableArray(ReadableArray readableArray) { + List deconstructedList = new ArrayList<>(readableArray.size()); + for (int i = 0; i < readableArray.size(); i++) { + ReadableType indexType = readableArray.getType(i); + switch (indexType) { + case Null: + deconstructedList.add(i, null); + break; + case Boolean: + deconstructedList.add(i, readableArray.getBoolean(i)); + break; + case Number: + deconstructedList.add(i, readableArray.getDouble(i)); + break; + case String: + deconstructedList.add(i, readableArray.getString(i)); + break; + case Map: + deconstructedList.add(i, Utils.recursivelyDeconstructReadableMap(readableArray.getMap(i))); + break; + case Array: + deconstructedList.add(i, Utils.recursivelyDeconstructReadableArray(readableArray.getArray(i))); + break; + default: + throw new IllegalArgumentException("Could not convert object at index " + i + "."); + } + } + return deconstructedList; + } +} diff --git a/android/src/main/java/io/invertase/firebase/analytics/RNFirebaseAnalytics.java b/android/src/main/java/io/invertase/firebase/analytics/RNFirebaseAnalytics.java new file mode 100644 index 00000000..10726b34 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/analytics/RNFirebaseAnalytics.java @@ -0,0 +1,103 @@ +package io.invertase.firebase.analytics; + +import android.util.Log; +import android.app.Activity; +import android.support.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.google.firebase.analytics.FirebaseAnalytics; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; + + +public class RNFirebaseAnalytics extends ReactContextBaseJavaModule { + + private static final String TAG = "RNFirebaseAnalytics"; + + public RNFirebaseAnalytics(ReactApplicationContext reactContext) { + super(reactContext); + Log.d(TAG, "New instance"); + } + + /** + * + * @return + */ + @Override + public String getName() { + return TAG; + } + + @ReactMethod + public void logEvent(final String name, @Nullable final ReadableMap params) { + FirebaseAnalytics.getInstance(getReactApplicationContext()).logEvent(name, Arguments.toBundle(params)); + } + + /** + * + * @param enabled + */ + @ReactMethod + public void setAnalyticsCollectionEnabled(final Boolean enabled) { + FirebaseAnalytics.getInstance(getReactApplicationContext()).setAnalyticsCollectionEnabled(enabled); + } + + /** + * + * @param screenName + * @param screenClassOverride + */ + @ReactMethod + public void setCurrentScreen(final String screenName, final String screenClassOverride) { + final Activity activity = getCurrentActivity(); + if (activity != null) { + // needs to be run on main thread + Log.d(TAG, "setCurrentScreen " + screenName + " - " + screenClassOverride); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + FirebaseAnalytics.getInstance(getReactApplicationContext()).setCurrentScreen(activity, screenName, screenClassOverride); + } + }); + } + } + + /** + * + * @param milliseconds + */ + @ReactMethod + public void setMinimumSessionDuration(final double milliseconds) { + FirebaseAnalytics.getInstance(getReactApplicationContext()).setMinimumSessionDuration((long) milliseconds); + } + + /** + * + * @param milliseconds + */ + @ReactMethod + public void setSessionTimeoutDuration(final double milliseconds) { + FirebaseAnalytics.getInstance(getReactApplicationContext()).setSessionTimeoutDuration((long) milliseconds); + } + + /** + * + * @param id + */ + @ReactMethod + public void setUserId(final String id) { + FirebaseAnalytics.getInstance(getReactApplicationContext()).setUserId(id); + } + + /** + * + * @param name + * @param value + */ + @ReactMethod + public void setUserProperty(final String name, final String value) { + FirebaseAnalytics.getInstance(getReactApplicationContext()).setUserProperty(name, value); + } +} diff --git a/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java b/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java new file mode 100644 index 00000000..c7def420 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuth.java @@ -0,0 +1,619 @@ + +package io.invertase.firebase.auth; + +import android.util.Log; + +import java.util.Map; + +import android.net.Uri; +import android.support.annotation.NonNull; + +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.ReactContext; + +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; + +import com.google.firebase.auth.AuthCredential; +import com.google.firebase.auth.AuthResult; +import com.google.firebase.auth.UserProfileChangeRequest; +import com.google.firebase.auth.FacebookAuthProvider; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.auth.GetTokenResult; +import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.EmailAuthProvider; + + +import io.invertase.firebase.Utils; + + +@SuppressWarnings("ThrowableResultOfMethodCallIgnored") +public class RNFirebaseAuth extends ReactContextBaseJavaModule { + private final int NO_CURRENT_USER = 100; + private final int ERROR_FETCHING_TOKEN = 101; + private final int ERROR_SENDING_VERIFICATION_EMAIL = 102; + + private static final String TAG = "RNFirebaseAuth"; + + // private Context context; + private ReactContext mReactContext; + private FirebaseAuth mAuth; + private FirebaseAuth.AuthStateListener mAuthListener; + + public RNFirebaseAuth(ReactApplicationContext reactContext) { + super(reactContext); + mReactContext = reactContext; + mAuth = FirebaseAuth.getInstance(); + + Log.d(TAG, "New RNFirebaseAuth instance"); + } + + @Override + public String getName() { + return TAG; + } + + /** + * Returns a no user error. + * + * @param callback JS callback + */ + private void callbackNoUser(Callback callback, Boolean isError) { + WritableMap err = Arguments.createMap(); + err.putInt("errorCode", NO_CURRENT_USER); + err.putString("errorMessage", "No current user"); + + if (isError) { + callback.invoke(err); + } else { + callback.invoke(null, null); + } + } + + @ReactMethod + public void listenForAuth() { + if (mAuthListener == null) { + mAuthListener = new FirebaseAuth.AuthStateListener() { + @Override + public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { + FirebaseUser user = firebaseAuth.getCurrentUser(); + WritableMap msgMap = Arguments.createMap(); + msgMap.putString("eventName", "listenForAuth"); + + if (user != null) { + // TODO move to helper + WritableMap userMap = getUserMap(user); + msgMap.putBoolean("authenticated", true); + msgMap.putMap("user", userMap); + + Utils.sendEvent(mReactContext, "listenForAuth", msgMap); + } else { + msgMap.putBoolean("authenticated", false); + Utils.sendEvent(mReactContext, "listenForAuth", msgMap); + } + } + }; + mAuth.addAuthStateListener(mAuthListener); + } + } + + @ReactMethod + public void unlistenForAuth(final Callback callback) { + if (mAuthListener != null) { + mAuth.removeAuthStateListener(mAuthListener); + + // TODO move to helper + WritableMap resp = Arguments.createMap(); + resp.putString("status", "complete"); + + callback.invoke(null, resp); + } + } + + @ReactMethod + public void createUserWithEmail(final String email, final String password, final Callback callback) { + mAuth.createUserWithEmailAndPassword(email, password) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + userCallback(task.getResult().getUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } + + @ReactMethod + public void signInWithEmail(final String email, final String password, final Callback callback) { + + mAuth.signInWithEmailAndPassword(email, password) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + userCallback(task.getResult().getUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } + + @ReactMethod + public void signInWithProvider(final String provider, final String authToken, final String authSecret, final Callback callback) { + if (provider.equals("facebook")) { + this.facebookLogin(authToken, callback); + } else if (provider.equals("google")) { + this.googleLogin(authToken, callback); + } else + // TODO + Utils.todoNote(TAG, "signInWithProvider", callback); + } + + @ReactMethod + public void linkPassword(final String email, final String password, final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user != null) { + AuthCredential credential = EmailAuthProvider.getCredential(email, password); + user + .linkWithCredential(credential) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + Log.d(TAG, "user linked with password credential"); + userCallback(mAuth.getCurrentUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + @ReactMethod + public void link(final String provider, final String authToken, final String authSecret, final Callback callback) { + if (provider.equals("password")) { + linkPassword(authToken, authSecret, callback); + } else + // TODO other providers + Utils.todoNote(TAG, "linkWithProvider", callback); + } + + @ReactMethod + public void signInAnonymously(final Callback callback) { + Log.d(TAG, "signInAnonymously:called:"); + mAuth.signInAnonymously() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + Log.d(TAG, "signInAnonymously:onComplete:" + task.isSuccessful()); + + try { + if (task.isSuccessful()) { + userCallback(task.getResult().getUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } + + @ReactMethod + public void signInWithCustomToken(final String customToken, final Callback callback) { + mAuth.signInWithCustomToken(customToken) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + Log.d(TAG, "signInWithCustomToken:onComplete:" + task.isSuccessful()); + try { + if (task.isSuccessful()) { + userCallback(task.getResult().getUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } + + @ReactMethod + public void reauthenticateWithCredentialForProvider(final String provider, final String authToken, final String authSecret, final Callback callback) { + // TODO: + Utils.todoNote(TAG, "reauthenticateWithCredentialForProvider", callback); + // AuthCredential credential; + // Log.d(TAG, "reauthenticateWithCredentialForProvider called with: " + provider); + } + + @ReactMethod + public void updateUserEmail(final String email, final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user != null) { + user + .updateEmail(email) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + Log.d(TAG, "User email address updated"); + userCallback(mAuth.getCurrentUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + @ReactMethod + public void updateUserPassword(final String newPassword, final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user != null) { + user.updatePassword(newPassword) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + Log.d(TAG, "User password updated"); + userCallback(mAuth.getCurrentUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + @ReactMethod + public void sendPasswordResetWithEmail(final String email, final Callback callback) { + mAuth.sendPasswordResetEmail(email) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + WritableMap resp = Arguments.createMap(); + resp.putString("status", "complete"); + callback.invoke(null, resp); + } else { + callback.invoke(task.getException().toString()); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } + + @ReactMethod + public void deleteUser(final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + if (user != null) { + user.delete() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + Log.d(TAG, "User account deleted"); + WritableMap resp = Arguments.createMap(); + resp.putString("status", "complete"); + resp.putString("msg", "User account deleted"); + callback.invoke(null, resp); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + + @ReactMethod + public void sendEmailVerification(final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user != null) { + user.sendEmailVerification() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + WritableMap resp = Arguments.createMap(); + resp.putString("status", "complete"); + resp.putString("msg", "User verification email sent"); + callback.invoke(null, resp); + } else { + WritableMap err = Arguments.createMap(); + err.putInt("errorCode", ERROR_SENDING_VERIFICATION_EMAIL); + err.putString("errorMessage", task.getException().getMessage()); + callback.invoke(err); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + + @ReactMethod + public void getToken(final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user != null) { + user.getToken(true) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + String token = task.getResult().getToken(); + WritableMap resp = Arguments.createMap(); + resp.putString("status", "complete"); + resp.putString("token", token); + callback.invoke(null, resp); + } else { + WritableMap err = Arguments.createMap(); + err.putInt("errorCode", ERROR_FETCHING_TOKEN); + err.putString("errorMessage", task.getException().getMessage()); + callback.invoke(err); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + @ReactMethod + public void updateUserProfile(ReadableMap props, final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user != null) { + UserProfileChangeRequest.Builder profileBuilder = new UserProfileChangeRequest.Builder(); + + Map m = Utils.recursivelyDeconstructReadableMap(props); + + if (m.containsKey("displayName")) { + String displayName = (String) m.get("displayName"); + profileBuilder.setDisplayName(displayName); + } + + if (m.containsKey("photoUri")) { + String photoUriStr = (String) m.get("photoUri"); + Uri uri = Uri.parse(photoUriStr); + profileBuilder.setPhotoUri(uri); + } + + UserProfileChangeRequest profileUpdates = profileBuilder.build(); + + user.updateProfile(profileUpdates) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + Log.d(TAG, "User profile updated"); + userCallback(mAuth.getCurrentUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + @ReactMethod + public void signOut(final Callback callback) { + mAuth.signOut(); + + WritableMap resp = Arguments.createMap(); + resp.putString("status", "complete"); + resp.putString("msg", "User signed out"); + callback.invoke(null, resp); + } + + @ReactMethod + public void reloadUser(final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user == null) { + callbackNoUser(callback, false); + } else { + user.reload() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "user reloaded"); + userCallback(mAuth.getCurrentUser(), callback); + } else { + userErrorCallback(task, callback); + } + } + }); + } + } + + @ReactMethod + public void getCurrentUser(final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user == null) { + callbackNoUser(callback, false); + } else { + Log.d("USRC", user.getUid()); + userCallback(user, callback); + } + } + + // TODO: Check these things + @ReactMethod + public void googleLogin(String IdToken, final Callback callback) { + AuthCredential credential = GoogleAuthProvider.getCredential(IdToken, null); + mAuth.signInWithCredential(credential) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + userCallback(task.getResult().getUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } + + @ReactMethod + public void facebookLogin(String Token, final Callback callback) { + AuthCredential credential = FacebookAuthProvider.getCredential(Token); + mAuth.signInWithCredential(credential) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + userCallback(task.getResult().getUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } + + // Internal helpers + private void userCallback(final FirebaseUser user, final Callback callback) { + if (user != null) { + user.getToken(true).addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + WritableMap userMap = getUserMap(user); + userMap.putString("token", task.getResult().getToken()); + callback.invoke(null, userMap); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + private void userErrorCallback(Task task, final Callback onFail) { + WritableMap error = Arguments.createMap(); + error.putString("code", ((FirebaseAuthException) task.getException()).getErrorCode()); + error.putString("message", task.getException().getMessage()); + onFail.invoke(error); + } + + private void userExceptionCallback(Exception ex, final Callback onFail) { + WritableMap error = Arguments.createMap(); + error.putInt("code", ex.hashCode()); + error.putString("message", ex.getMessage()); + onFail.invoke(error); + } + + private WritableMap getUserMap(FirebaseUser user) { + WritableMap userMap = Arguments.createMap(); + if (user != null) { + final String email = user.getEmail(); + final String uid = user.getUid(); + final String provider = user.getProviderId(); + final String name = user.getDisplayName(); + final Boolean verified = user.isEmailVerified(); + final Uri photoUrl = user.getPhotoUrl(); + + userMap.putString("email", email); + userMap.putString("uid", uid); + userMap.putString("providerId", provider); + userMap.putBoolean("emailVerified", verified); + + if (name != null) { + userMap.putString("name", name); + } + + if (photoUrl != null) { + userMap.putString("photoURL", photoUrl.toString()); + } + } else { + userMap.putString("msg", "no user"); + } + + return userMap; + } +} diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java new file mode 100644 index 00000000..2a0d0ed2 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java @@ -0,0 +1,368 @@ +package io.invertase.firebase.database; + +import java.util.List; +import java.util.Map; + +import android.net.Uri; +import android.util.Log; + +import java.util.HashMap; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReadableMapKeySetIterator; + +import com.google.firebase.database.OnDisconnect; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ServerValue; + + +import io.invertase.firebase.Utils; + +public class RNFirebaseDatabase extends ReactContextBaseJavaModule { + private static final String TAG = "RNFirebaseDatabase"; + private HashMap mDBListeners = new HashMap(); + private FirebaseDatabase mFirebaseDatabase; + + public RNFirebaseDatabase(ReactApplicationContext reactContext) { + super(reactContext); + mFirebaseDatabase = FirebaseDatabase.getInstance(); + } + + @Override + public String getName() { + return TAG; + } + + // Persistence + @ReactMethod + public void enablePersistence( + final Boolean enable, + final Callback callback) { + try { + mFirebaseDatabase.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 = mFirebaseDatabase.getReference(path); + ref.keepSynced(enable); + + WritableMap res = Arguments.createMap(); + res.putString("status", "success"); + res.putString("path", path); + callback.invoke(null, res); + } + + // RNFirebaseDatabase + @ReactMethod + public void set( + final String path, + final ReadableMap props, + final Callback callback) { + DatabaseReference ref = mFirebaseDatabase.getReference(path); + Map m = Utils.recursivelyDeconstructReadableMap(props); + + + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handleCallback("set", callback, error); + } + }; + + ref.setValue(m.get("value"), listener); + } + + @ReactMethod + public void update(final String path, + final ReadableMap props, + final Callback callback) { + DatabaseReference ref = mFirebaseDatabase.getReference(path); + Map m = Utils.recursivelyDeconstructReadableMap(props); + + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handleCallback("update", callback, error); + } + }; + + ref.updateChildren(m, listener); + } + + @ReactMethod + public void remove(final String path, + final Callback callback) { + DatabaseReference ref = mFirebaseDatabase.getReference(path); + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handleCallback("remove", callback, error); + } + }; + + 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 = mFirebaseDatabase.getReference(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 + Map m = Utils.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.get("value"), listener); + } else { + Log.d(TAG, "No value passed to push: " + newPath); + WritableMap res = Arguments.createMap(); + res.putString("status", "success"); + res.putString("ref", newPath); + callback.invoke(null, res); + } + } + + @ReactMethod + public void on(final String path, + final String modifiersString, + final ReadableArray modifiersArray, + final String eventName, + final Callback callback) { + RNFirebaseDatabaseReference ref = this.getDBHandle(path, modifiersArray, modifiersString); + + if (eventName.equals("value")) { + ref.addValueEventListener(); + } else { + ref.addChildEventListener(eventName); + } + + WritableMap resp = Arguments.createMap(); + resp.putString("status", "success"); + resp.putString("handle", path); + callback.invoke(null, resp); + } + + @ReactMethod + public void onOnce(final String path, + final String modifiersString, + final ReadableArray modifiersArray, + final String eventName, + final Callback callback) { + RNFirebaseDatabaseReference ref = this.getDBHandle(path, modifiersArray, modifiersString); + ref.addOnceValueEventListener(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, + final String modifiersString, + final String eventName, + final Callback callback) { + + String key = this.getDBListenerKey(path, modifiersString); + RNFirebaseDatabaseReference r = mDBListeners.get(key); + + if (r != null) { + if (eventName == null || "".equals(eventName)) { + r.cleanup(); + mDBListeners.remove(key); + } else { + r.removeEventListener(eventName); + if (!r.hasListeners()) { + mDBListeners.remove(key); + } + } + } + + Log.d(TAG, "Removed listener " + path + "/" + modifiersString); + WritableMap resp = Arguments.createMap(); + resp.putString("handle", path); + resp.putString("status", "success"); + resp.putString("modifiersString", modifiersString); + //TODO: Remaining listeners + callback.invoke(null, resp); + } + + @ReactMethod + public void onDisconnectSet(final String path, final ReadableMap props, final Callback callback) { + String type = props.getString("type"); + DatabaseReference ref = mFirebaseDatabase.getReference(path); + OnDisconnect od = ref.onDisconnect(); + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handleCallback("onDisconnectSet", callback, error); + } + }; + + switch (type) { + case "object": + Map map = Utils.recursivelyDeconstructReadableMap(props.getMap("value")); + od.setValue(map, listener); + break; + case "array": + List list = Utils.recursivelyDeconstructReadableArray(props.getArray("value")); + od.setValue(list, listener); + break; + case "string": + od.setValue(props.getString("value"), listener); + break; + case "number": + od.setValue(props.getDouble("value"), listener); + break; + case "boolean": + od.setValue(props.getBoolean("value"), listener); + break; + case "null": + od.setValue(null, listener); + break; + } + } + + @ReactMethod + public void onDisconnectUpdate(final String path, final ReadableMap props, final Callback callback) { + DatabaseReference ref = mFirebaseDatabase.getReference(path); + OnDisconnect od = ref.onDisconnect(); + Map map = Utils.recursivelyDeconstructReadableMap(props); + od.updateChildren(map, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handleCallback("onDisconnectUpdate", callback, error); + } + }); + } + + @ReactMethod + public void onDisconnectRemove(final String path, final Callback callback) { + DatabaseReference ref = mFirebaseDatabase.getReference(path); + + OnDisconnect od = ref.onDisconnect(); + od.removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handleCallback("onDisconnectRemove", callback, error); + } + }); + } + + @ReactMethod + public void onDisconnectCancel(final String path, final Callback callback) { + DatabaseReference ref = mFirebaseDatabase.getReference(path); + + OnDisconnect od = ref.onDisconnect(); + od.cancel(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handleCallback("onDisconnectCancel", callback, error); + } + }); + } + + @ReactMethod + public void goOnline() { + mFirebaseDatabase.goOnline(); + } + + @ReactMethod + public void goOffline() { + mFirebaseDatabase.goOffline(); + } + + private void handleCallback( + final String methodName, + final Callback callback, + final DatabaseError databaseError) { + 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 RNFirebaseDatabaseReference getDBHandle(final String path, + final ReadableArray modifiersArray, + final String modifiersString) { + String key = this.getDBListenerKey(path, modifiersString); + RNFirebaseDatabaseReference r = mDBListeners.get(key); + + if (r == null) { + ReactContext ctx = getReactApplicationContext(); + r = new RNFirebaseDatabaseReference(ctx, mFirebaseDatabase, path, modifiersArray, modifiersString); + mDBListeners.put(key, r); + } + + return r; + } + + private String getDBListenerKey(String path, String modifiersString) { + return path + " | " + modifiersString; + } + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + constants.put("serverValueTimestamp", ServerValue.TIMESTAMP); + return constants; + } +} diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java new file mode 100644 index 00000000..6580d21a --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java @@ -0,0 +1,296 @@ +package io.invertase.firebase.database; + +import java.util.HashSet; +import java.util.List; +import android.util.Log; +import java.util.ListIterator; +import java.util.Set; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; + +import com.google.firebase.database.Query; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.ValueEventListener; + +import io.invertase.firebase.Utils; + +public class RNFirebaseDatabaseReference { + private static final String TAG = "RNFirebaseDBReference"; + + private Query mQuery; + private String mPath; + private String mModifiersString; + private ChildEventListener mEventListener; + private ValueEventListener mValueListener; + private ReactContext mReactContext; + private Set childEventListeners = new HashSet<>(); + + public RNFirebaseDatabaseReference(final ReactContext context, + final FirebaseDatabase firebaseDatabase, + final String path, + final ReadableArray modifiersArray, + final String modifiersString) { + mReactContext = context; + mPath = path; + mModifiersString = modifiersString; + mQuery = this.buildDatabaseQueryAtPathAndModifiers(firebaseDatabase, path, modifiersArray); + } + + public void addChildEventListener(final String eventName) { + if (mEventListener == null) { + mEventListener = new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { + handleDatabaseEvent("child_added", dataSnapshot); + } + + @Override + public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { + handleDatabaseEvent("child_changed", dataSnapshot); + } + + @Override + public void onChildRemoved(DataSnapshot dataSnapshot) { + handleDatabaseEvent("child_removed", dataSnapshot); + } + + @Override + public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { + handleDatabaseEvent("child_moved", dataSnapshot); + } + + @Override + public void onCancelled(DatabaseError error) { + handleDatabaseError(error); + } + }; + mQuery.addChildEventListener(mEventListener); + Log.d(TAG, "Added ChildEventListener for path: " + mPath + " with modifiers: "+ mModifiersString); + } else { + Log.w(TAG, "Trying to add duplicate ChildEventListener for path: " + mPath + " with modifiers: "+ mModifiersString); + } + //Keep track of the events that the JS is interested in knowing about + childEventListeners.add(eventName); + } + + public void addValueEventListener() { + if (mValueListener == null) { + mValueListener = new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + handleDatabaseEvent("value", dataSnapshot); + } + + @Override + public void onCancelled(DatabaseError error) { + handleDatabaseError(error); + } + }; + mQuery.addValueEventListener(mValueListener); + Log.d(TAG, "Added ValueEventListener for path: " + mPath + " with modifiers: "+ mModifiersString); + //this.setListeningTo(mPath, modifiersString, "value"); + } else { + Log.w(TAG, "Trying to add duplicate ValueEventListener for path: " + mPath + " with modifiers: "+ mModifiersString); + } + } + + public void addOnceValueEventListener(final Callback callback) { + final ValueEventListener onceValueEventListener = new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + WritableMap data = Utils.dataSnapshotToMap("value", mPath, mModifiersString, 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); + } + }; + mQuery.addListenerForSingleValueEvent(onceValueEventListener); + Log.d(TAG, "Added OnceValueEventListener for path: " + mPath + " with modifiers " + mModifiersString); + } + + public void removeEventListener(String eventName) { + if ("value".equals(eventName)) { + this.removeValueEventListener(); + } else { + childEventListeners.remove(eventName); + if (childEventListeners.isEmpty()) { + this.removeChildEventListener(); + } + } + } + + public boolean hasListeners() { + return mEventListener != null || mValueListener != null; + } + + public void cleanup() { + Log.d(TAG, "cleaning up database reference " + this); + childEventListeners.clear(); + this.removeChildEventListener(); + this.removeValueEventListener(); + } + + private void removeChildEventListener() { + if (mEventListener != null) { + mQuery.removeEventListener(mEventListener); + mEventListener = null; + } + } + + private void removeValueEventListener() { + if (mValueListener != null) { + mQuery.removeEventListener(mValueListener); + mValueListener = null; + } + } + + private void handleDatabaseEvent(final String name, final DataSnapshot dataSnapshot) { + WritableMap data = Utils.dataSnapshotToMap(name, mPath, mModifiersString, dataSnapshot); + WritableMap evt = Arguments.createMap(); + evt.putString("eventName", name); + evt.putMap("body", data); + + Utils.sendEvent(mReactContext, "database_event", evt); + } + + private void handleDatabaseError(final DatabaseError error) { + WritableMap err = Arguments.createMap(); + err.putString("eventName", "database_error"); + err.putString("path", mPath); + err.putString("modifiersString", mModifiersString); + err.putInt("errorCode", error.getCode()); + err.putString("errorDetails", error.getDetails()); + err.putString("msg", error.getMessage()); + + WritableMap evt = Arguments.createMap(); + evt.putString("eventName", "database_error"); + evt.putMap("body", err); + + Utils.sendEvent(mReactContext, "database_error", evt); + } + + private Query buildDatabaseQueryAtPathAndModifiers(final FirebaseDatabase firebaseDatabase, + final String path, + final ReadableArray modifiers) { + Query query = firebaseDatabase.getReference(path); + List strModifiers = Utils.recursivelyDeconstructReadableArray(modifiers); + ListIterator it = strModifiers.listIterator(); + + while(it.hasNext()) { + String str = (String) it.next(); + + String[] strArr = str.split(":"); + String methStr = strArr[0]; + + if (methStr.equalsIgnoreCase("orderByKey")) { + query = query.orderByKey(); + } else if (methStr.equalsIgnoreCase("orderByValue")) { + query = query.orderByValue(); + } else if (methStr.equalsIgnoreCase("orderByPriority")) { + query = query.orderByPriority(); + } else if (methStr.contains("orderByChild")) { + String key = strArr[1]; + Log.d(TAG, "orderByChild: " + key); + query = query.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 type = strArr[2]; + if ("number".equals(type)) { + double doubleValue = Double.parseDouble(value); + if (strArr.length > 3) { + query = query.equalTo(doubleValue, strArr[3]); + } else { + query = query.equalTo(doubleValue); + } + } else if ("boolean".equals(type)) { + boolean booleanValue = Boolean.parseBoolean(value); + if (strArr.length > 3) { + query = query.equalTo(booleanValue, strArr[3] ); + } else { + query = query.equalTo(booleanValue); + } + } else { + if (strArr.length > 3) { + query = query.equalTo(value, strArr[3]); + } else { + query = query.equalTo(value); + } + } + } else if (methStr.contains("endAt")) { + String value = strArr[1]; + String type = strArr[2]; + if ("number".equals(type)) { + double doubleValue = Double.parseDouble(value); + if (strArr.length > 3) { + query = query.endAt(doubleValue, strArr[3]); + } else { + query = query.endAt(doubleValue); + } + } else if ("boolean".equals(type)) { + boolean booleanValue = Boolean.parseBoolean(value); + if (strArr.length > 3) { + query = query.endAt(booleanValue, strArr[3] ); + } else { + query = query.endAt(booleanValue); + } + } else { + if (strArr.length > 3) { + query = query.endAt(value, strArr[3]); + } else { + query = query.endAt(value); + } + } + } else if (methStr.contains("startAt")) { + String value = strArr[1]; + String type = strArr[2]; + if ("number".equals(type)) { + double doubleValue = Double.parseDouble(value); + if (strArr.length > 3) { + query = query.startAt(doubleValue, strArr[3]); + } else { + query = query.startAt(doubleValue); + } + } else if ("boolean".equals(type)) { + boolean booleanValue = Boolean.parseBoolean(value); + if (strArr.length > 3) { + query = query.startAt(booleanValue, strArr[3] ); + } else { + query = query.startAt(booleanValue); + } + } else { + if (strArr.length > 3) { + query = query.startAt(value, strArr[3]); + } else { + query = query.startAt(value); + } + } + } + } + + return query; + } +} diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java new file mode 100644 index 00000000..9122539e --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -0,0 +1,229 @@ +package io.invertase.firebase.messaging; + +import java.util.Map; + +import android.content.Context; +import android.content.IntentFilter; +import android.content.Intent; +import android.content.BroadcastReceiver; +import android.util.Log; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableMap; + +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.RemoteMessage; + +import io.invertase.firebase.Utils; + +public class RNFirebaseMessaging extends ReactContextBaseJavaModule { + + private static final String TAG = "RNFirebaseMessaging"; + private static final String EVENT_NAME_TOKEN = "RNFirebaseRefreshToken"; + private static final String EVENT_NAME_NOTIFICATION = "RNFirebaseReceiveNotification"; + private static final String EVENT_NAME_SEND = "RNFirebaseUpstreamSend"; + + public static final String INTENT_NAME_TOKEN = "io.invertase.firebase.refreshToken"; + public static final String INTENT_NAME_NOTIFICATION = "io.invertase.firebase.ReceiveNotification"; + public static final String INTENT_NAME_SEND = "io.invertase.firebase.Upstream"; + + private IntentFilter mRefreshTokenIntentFilter; + private IntentFilter mReceiveNotificationIntentFilter; + private IntentFilter mReceiveSendIntentFilter; + private BroadcastReceiver mBroadcastReceiver; + + public RNFirebaseMessaging(ReactApplicationContext reactContext) { + super(reactContext); + mRefreshTokenIntentFilter = new IntentFilter(INTENT_NAME_TOKEN); + mReceiveNotificationIntentFilter = new IntentFilter(INTENT_NAME_NOTIFICATION); + mReceiveSendIntentFilter = new IntentFilter(INTENT_NAME_SEND); + initRefreshTokenHandler(); + initMessageHandler(); + initSendHandler(); + Log.d(TAG, "New instance"); + } + + @Override + public String getName() { + return TAG; + } + + private void initMessageHandler() { + Log.d(TAG, "RNFirebase initMessageHandler called"); + + if (mBroadcastReceiver == null) { + mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + RemoteMessage remoteMessage = intent.getParcelableExtra("data"); + Log.d(TAG, "Firebase onReceive: " + remoteMessage); + WritableMap params = Arguments.createMap(); + + params.putNull("data"); + params.putNull("notification"); + params.putString("id", remoteMessage.getMessageId()); + params.putString("messageId", remoteMessage.getMessageId()); + + + if (remoteMessage.getData().size() != 0) { + WritableMap dataMap = Arguments.createMap(); + Map data = remoteMessage.getData(); + + for (String key : data.keySet()) { + dataMap.putString(key, data.get(key)); + } + + params.putMap("data", dataMap); + } + + + if (remoteMessage.getNotification() != null) { + WritableMap notificationMap = Arguments.createMap(); + RemoteMessage.Notification notification = remoteMessage.getNotification(); + notificationMap.putString("title", notification.getTitle()); + notificationMap.putString("body", notification.getBody()); + notificationMap.putString("icon", notification.getIcon()); + notificationMap.putString("sound", notification.getSound()); + notificationMap.putString("tag", notification.getTag()); + params.putMap("notification", notificationMap); + } + + ReactContext ctx = getReactApplicationContext(); + Utils.sendEvent(ctx, EVENT_NAME_NOTIFICATION, params); + } + }; + + } + getReactApplicationContext().registerReceiver(mBroadcastReceiver, mReceiveNotificationIntentFilter); + } + + /** + * + */ + private void initRefreshTokenHandler() { + getReactApplicationContext().registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + WritableMap params = Arguments.createMap(); + params.putString("token", intent.getStringExtra("token")); + ReactContext ctx = getReactApplicationContext(); + Log.d(TAG, "initRefreshTokenHandler received event " + EVENT_NAME_TOKEN); + Utils.sendEvent(ctx, EVENT_NAME_TOKEN, params); + } + + ; + }, mRefreshTokenIntentFilter); + } + + @ReactMethod + public void subscribeToTopic(String topic, final Callback callback) { + try { + FirebaseMessaging.getInstance().subscribeToTopic(topic); + callback.invoke(null, topic); + } catch (Exception e) { + e.printStackTrace(); + Log.d(TAG, "Firebase token: " + e); + WritableMap error = Arguments.createMap(); + error.putString("message", e.getMessage()); + callback.invoke(error); + + } + } + + @ReactMethod + public void getToken(final Callback callback) { + + try { + String token = FirebaseInstanceId.getInstance().getToken(); + Log.d(TAG, "Firebase token: " + token); + callback.invoke(null, token); + } catch (Exception e) { + WritableMap error = Arguments.createMap(); + error.putString("message", e.getMessage()); + callback.invoke(error); + } + } + + @ReactMethod + public void unsubscribeFromTopic(String topic, final Callback callback) { + try { + FirebaseMessaging.getInstance().unsubscribeFromTopic(topic); + callback.invoke(null, topic); + } catch (Exception e) { + WritableMap error = Arguments.createMap(); + error.putString("message", e.getMessage()); + callback.invoke(error); + } + } + + // String senderId, String messageId, String messageType, + @ReactMethod + public void send(ReadableMap params, final Callback callback) { + ReadableMap data = params.getMap("data"); + FirebaseMessaging fm = FirebaseMessaging.getInstance(); + RemoteMessage.Builder remoteMessage = new RemoteMessage.Builder(params.getString("sender")); + + remoteMessage.setMessageId(params.getString("id")); + remoteMessage.setMessageType(params.getString("type")); + + if (params.hasKey("ttl")) { + remoteMessage.setTtl(params.getInt("ttl")); + } + + if (params.hasKey("collapseKey")) { + remoteMessage.setCollapseKey(params.getString("collapseKey")); + } + + ReadableMapKeySetIterator iterator = data.keySetIterator(); + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = data.getType(key); + if (type == ReadableType.String) { + remoteMessage.addData(key, data.getString(key)); + } + } + + try { + fm.send(remoteMessage.build()); + WritableMap res = Arguments.createMap(); + res.putString("status", "success"); + Log.d(TAG, "send: Message sent"); + callback.invoke(null, res); + } catch (Exception e) { + Log.e(TAG, "send: error sending message", e); + WritableMap error = Arguments.createMap(); + error.putString("code", e.toString()); + error.putString("message", e.toString()); + callback.invoke(error); + } + } + + private void initSendHandler() { + getReactApplicationContext().registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + WritableMap params = Arguments.createMap(); + if (intent.getBooleanExtra("hasError", false)) { + WritableMap error = Arguments.createMap(); + error.putInt("code", intent.getIntExtra("errCode", 0)); + error.putString("message", intent.getStringExtra("errorMessage")); + params.putMap("err", error); + } else { + params.putNull("err"); + } + ReactContext ctx = getReactApplicationContext(); + Utils.sendEvent(ctx, EVENT_NAME_SEND, params); + } + }, mReceiveSendIntentFilter); + } +} diff --git a/android/src/main/java/io/invertase/firebase/storage/RNFirebaseStorage.java b/android/src/main/java/io/invertase/firebase/storage/RNFirebaseStorage.java new file mode 100644 index 00000000..226a079f --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/storage/RNFirebaseStorage.java @@ -0,0 +1,484 @@ +package io.invertase.firebase.storage; + +import android.util.Log; +import android.os.Environment; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.HashMap; + +import android.net.Uri; +import android.database.Cursor; +import android.provider.MediaStore; +import android.support.annotation.NonNull; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; + +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.storage.StorageException; +import com.google.firebase.storage.StorageTask; +import com.google.firebase.storage.StreamDownloadTask; +import com.google.firebase.storage.UploadTask; +import com.google.firebase.storage.FirebaseStorage; +import com.google.firebase.storage.StorageMetadata; +import com.google.firebase.storage.StorageReference; +import com.google.firebase.storage.OnPausedListener; +import com.google.firebase.storage.OnProgressListener; + +import io.invertase.firebase.Utils; + + +@SuppressWarnings("WeakerAccess") +public class RNFirebaseStorage extends ReactContextBaseJavaModule { + + private static final String TAG = "RNFirebaseStorage"; + private static final String DocumentDirectoryPath = "DOCUMENT_DIRECTORY_PATH"; + private static final String ExternalDirectoryPath = "EXTERNAL_DIRECTORY_PATH"; + private static final String ExternalStorageDirectoryPath = "EXTERNAL_STORAGE_DIRECTORY_PATH"; + private static final String PicturesDirectoryPath = "PICTURES_DIRECTORY_PATH"; + private static final String TemporaryDirectoryPath = "TEMPORARY_DIRECTORY_PATH"; + private static final String CachesDirectoryPath = "CACHES_DIRECTORY_PATH"; + private static final String DocumentDirectory = "DOCUMENT_DIRECTORY_PATH"; + + private static final String FileTypeRegular = "FILETYPE_REGULAR"; + private static final String FileTypeDirectory = "FILETYPE_DIRECTORY"; + + private static final String STORAGE_EVENT = "storage_event"; + private static final String STORAGE_ERROR = "storage_error"; + private static final String STORAGE_STATE_CHANGED = "state_changed"; + private static final String STORAGE_UPLOAD_SUCCESS = "upload_success"; + private static final String STORAGE_UPLOAD_FAILURE = "upload_failure"; + private static final String STORAGE_DOWNLOAD_SUCCESS = "download_success"; + private static final String STORAGE_DOWNLOAD_FAILURE = "download_failure"; + + private ReactContext mReactContext; + + public RNFirebaseStorage(ReactApplicationContext reactContext) { + super(reactContext); + + Log.d(TAG, "New instance"); + } + + @Override + public String getName() { + return TAG; + } + + + public boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + @ReactMethod + public void delete(final String path, + final Callback callback) { + StorageReference reference = this.getReference(path); + reference.delete().addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void aVoid) { + WritableMap data = Arguments.createMap(); + data.putString("success", "success"); + data.putString("path", path); + callback.invoke(null, data); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception exception) { + callback.invoke(makeErrorPayload(1, exception)); + } + }); + } + + @ReactMethod + public void getDownloadURL(final String path, + final Callback callback) { + Log.d(TAG, "Download url for remote path: " + path); + final StorageReference reference = this.getReference(path); + + Task downloadTask = reference.getDownloadUrl(); + downloadTask + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Uri uri) { + callback.invoke(null, uri.toString()); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception exception) { + callback.invoke(makeErrorPayload(1, exception)); + } + }); + } + + @ReactMethod + public void getMetadata(final String path, + final Callback callback) { + StorageReference reference = this.getReference(path); + reference.getMetadata().addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(StorageMetadata storageMetadata) { + WritableMap data = getMetadataAsMap(storageMetadata); + callback.invoke(null, data); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception exception) { + callback.invoke(makeErrorPayload(1, exception)); + } + }); + } + + @ReactMethod + public void updateMetadata(final String path, + final ReadableMap metadata, + final Callback callback) { + StorageReference reference = this.getReference(path); + StorageMetadata md = buildMetadataFromMap(metadata); + reference.updateMetadata(md).addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(StorageMetadata storageMetadata) { + WritableMap data = getMetadataAsMap(storageMetadata); + callback.invoke(null, data); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception exception) { + callback.invoke(makeErrorPayload(1, exception)); + } + }); + } + + @ReactMethod + public void downloadFile(final String path, + final String localPath, + final Callback callback) { + if (!isExternalStorageWritable()) { + Log.w(TAG, "downloadFile failed: external storage not writable"); + WritableMap error = Arguments.createMap(); + final int errorCode = 1; + error.putDouble("code", errorCode); + error.putString("description", "downloadFile failed: external storage not writable"); + callback.invoke(error); + return; + } + Log.d(TAG, "downloadFile from remote path: " + path); + + StorageReference reference = this.getReference(path); + + reference.getStream(new StreamDownloadTask.StreamProcessor() { + @Override + public void doInBackground(StreamDownloadTask.TaskSnapshot taskSnapshot, InputStream inputStream) throws IOException { + int indexOfLastSlash = localPath.lastIndexOf("/"); + String pathMinusFileName = indexOfLastSlash>0 ? localPath.substring(0, indexOfLastSlash) + "/" : "/"; + String filename = indexOfLastSlash>0 ? localPath.substring(indexOfLastSlash+1) : localPath; + File fileWithJustPath = new File(pathMinusFileName); + fileWithJustPath.mkdirs(); + File fileWithFullPath = new File(pathMinusFileName, filename); + FileOutputStream output = new FileOutputStream(fileWithFullPath); + int bufferSize = 1024; + byte[] buffer = new byte[bufferSize]; + int len = 0; + while ((len = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, len); + } + output.close(); + } + }).addOnProgressListener(new OnProgressListener() { + @Override + public void onProgress(StreamDownloadTask.TaskSnapshot taskSnapshot) { + Log.d(TAG, "Got download progress " + taskSnapshot); + WritableMap event = getDownloadTaskAsMap(taskSnapshot); + handleStorageEvent(STORAGE_STATE_CHANGED, path, event); + } + }).addOnPausedListener(new OnPausedListener() { + @Override + public void onPaused(StreamDownloadTask.TaskSnapshot taskSnapshot) { + Log.d(TAG, "Download is paused " + taskSnapshot); + WritableMap event = getDownloadTaskAsMap(taskSnapshot); + handleStorageEvent(STORAGE_STATE_CHANGED, path, event); + } + }).addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(StreamDownloadTask.TaskSnapshot taskSnapshot) { + Log.d(TAG, "Successfully downloaded file " + taskSnapshot); + WritableMap resp = getDownloadTaskAsMap(taskSnapshot); + handleStorageEvent(STORAGE_DOWNLOAD_SUCCESS, path, resp); + //TODO: A little hacky, but otherwise throws a not consumed exception + resp = getDownloadTaskAsMap(taskSnapshot); + callback.invoke(null, resp); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception exception) { + Log.e(TAG, "Failed to download file " + exception.getMessage()); + //TODO: JS Error event + callback.invoke(makeErrorPayload(1, exception)); + } + }); + } + + @ReactMethod + public void putFile(final String path, final String localPath, final ReadableMap metadata, final Callback callback) { + StorageReference reference = this.getReference(path); + + Log.i(TAG, "Upload file: " + localPath + " to " + path); + + try { + Uri file; + if (localPath.startsWith("content://")) { + String realPath = getRealPathFromURI(localPath); + file = Uri.fromFile(new File(realPath)); + } else { + file = Uri.fromFile(new File(localPath)); + } + + StorageMetadata md = buildMetadataFromMap(metadata); + UploadTask uploadTask = reference.putFile(file, md); + + // register observers to listen for when the download is done or if it fails + uploadTask + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception exception) { + // handle unsuccessful uploads + Log.e(TAG, "Failed to upload file " + exception.getMessage()); + //TODO: JS Error event + callback.invoke(makeErrorPayload(1, exception)); + } + }) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) { + Log.d(TAG, "Successfully uploaded file " + taskSnapshot); + WritableMap resp = getUploadTaskAsMap(taskSnapshot); + handleStorageEvent(STORAGE_UPLOAD_SUCCESS, path, resp); + //TODO: A little hacky, but otherwise throws a not consumed exception + resp = getUploadTaskAsMap(taskSnapshot); + callback.invoke(null, resp); + } + }) + .addOnProgressListener(new OnProgressListener() { + @Override + public void onProgress(UploadTask.TaskSnapshot taskSnapshot) { + Log.d(TAG, "Got upload progress " + taskSnapshot); + WritableMap event = getUploadTaskAsMap(taskSnapshot); + handleStorageEvent(STORAGE_STATE_CHANGED, path, event); + } + }) + .addOnPausedListener(new OnPausedListener() { + @Override + public void onPaused(UploadTask.TaskSnapshot taskSnapshot) { + Log.d(TAG, "Upload is paused " + taskSnapshot); + WritableMap event = getUploadTaskAsMap(taskSnapshot); + handleStorageEvent(STORAGE_STATE_CHANGED, path, event); + } + }); + } catch (Exception ex) { + final int errorCode = 2; + callback.invoke(makeErrorPayload(errorCode, ex)); + } + } + + //Firebase.Storage methods + @ReactMethod + public void setMaxDownloadRetryTime(final double milliseconds) { + FirebaseStorage.getInstance().setMaxDownloadRetryTimeMillis((long)milliseconds); + } + + @ReactMethod + public void setMaxOperationRetryTime(final double milliseconds) { + FirebaseStorage.getInstance().setMaxOperationRetryTimeMillis((long)milliseconds); + } + + @ReactMethod + public void setMaxUploadRetryTime(final double milliseconds) { + FirebaseStorage.getInstance().setMaxUploadRetryTimeMillis((long)milliseconds); + } + + private StorageReference getReference(String path) { + if (path.startsWith("url::")) { + String url = path.substring(5); + return FirebaseStorage.getInstance().getReferenceFromUrl(url); + } else { + return FirebaseStorage.getInstance().getReference(path); + } + } + + private StorageMetadata buildMetadataFromMap(ReadableMap metadata) { + StorageMetadata.Builder metadataBuilder = new StorageMetadata.Builder(); + Map m = Utils.recursivelyDeconstructReadableMap(metadata); + + for (Map.Entry entry : m.entrySet()) { + metadataBuilder.setCustomMetadata(entry.getKey(), entry.getValue().toString()); + } + + return metadataBuilder.build(); + } + + private WritableMap getMetadataAsMap(StorageMetadata storageMetadata) { + WritableMap metadata = Arguments.createMap(); + metadata.putString("bucket", storageMetadata.getBucket()); + metadata.putString("generation", storageMetadata.getGeneration()); + metadata.putString("metageneration", storageMetadata.getMetadataGeneration()); + metadata.putString("fullPath", storageMetadata.getPath()); + metadata.putString("name", storageMetadata.getName()); + metadata.putDouble("size", storageMetadata.getSizeBytes()); + metadata.putDouble("timeCreated", storageMetadata.getCreationTimeMillis()); + metadata.putDouble("updated", storageMetadata.getUpdatedTimeMillis()); + metadata.putString("md5hash", storageMetadata.getMd5Hash()); + metadata.putString("cacheControl", storageMetadata.getCacheControl()); + metadata.putString("contentDisposition", storageMetadata.getContentDisposition()); + metadata.putString("contentEncoding", storageMetadata.getContentEncoding()); + metadata.putString("contentLanguage", storageMetadata.getContentLanguage()); + metadata.putString("contentType", storageMetadata.getContentType()); + + WritableArray downloadURLs = Arguments.createArray(); + for (Uri uri : storageMetadata.getDownloadUrls()) { + downloadURLs.pushString(uri.getPath()); + } + metadata.putArray("downloadURLs", downloadURLs); + + WritableMap customMetadata = Arguments.createMap(); + for (String key : storageMetadata.getCustomMetadataKeys()) { + customMetadata.putString(key, storageMetadata.getCustomMetadata(key)); + } + metadata.putMap("customMetadata", customMetadata); + + return metadata; + } + + private String getRealPathFromURI(final String uri) { + Cursor cursor = null; + try { + String[] proj = {MediaStore.Images.Media.DATA}; + cursor = getReactApplicationContext().getContentResolver().query(Uri.parse(uri), proj, null, null, null); + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + cursor.moveToFirst(); + return cursor.getString(column_index); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private WritableMap getDownloadTaskAsMap(final StreamDownloadTask.TaskSnapshot taskSnapshot) { + WritableMap resp = Arguments.createMap(); + resp.putDouble("bytesTransferred", taskSnapshot.getBytesTransferred()); + resp.putString("ref", taskSnapshot.getStorage().getPath()); + resp.putString("state", this.getTaskStatus(taskSnapshot.getTask())); + resp.putDouble("totalBytes", taskSnapshot.getTotalByteCount()); + + return resp; + } + + private WritableMap getUploadTaskAsMap(final UploadTask.TaskSnapshot taskSnapshot) { + StorageMetadata d = taskSnapshot.getMetadata(); + + WritableMap resp = Arguments.createMap(); + resp.putDouble("bytesTransferred", taskSnapshot.getBytesTransferred()); + resp.putString("downloadUrl", taskSnapshot.getDownloadUrl() != null ? taskSnapshot.getDownloadUrl().toString() : null); + resp.putString("ref", taskSnapshot.getStorage().getPath()); + resp.putString("state", this.getTaskStatus(taskSnapshot.getTask())); + resp.putDouble("totalBytes", taskSnapshot.getTotalByteCount()); + + if (taskSnapshot.getMetadata() != null) { + WritableMap metadata = getMetadataAsMap(taskSnapshot.getMetadata()); + resp.putMap("metadata", metadata); + } + + return resp; + } + + private String getTaskStatus(StorageTask task) { + if (task.isInProgress()) { + return "RUNNING"; + } else if (task.isPaused()) { + return "PAUSED"; + } else if (task.isSuccessful() || task.isComplete()) { + return "SUCCESS"; + } else if (task.isCanceled()) { + return "CANCELLED"; + } else if (task.getException() != null) { + return "ERROR"; + } else { + return "UNKNOWN"; + } + } + + private void handleStorageEvent(final String name, final String path, WritableMap body) { + WritableMap evt = Arguments.createMap(); + evt.putString("eventName", name); + evt.putString("path", path); + evt.putMap("body", body); + + Utils.sendEvent(this.getReactApplicationContext(), STORAGE_EVENT, evt); + } + + private void handleStorageError(final String path, final StorageException error) { + WritableMap body = Arguments.createMap(); + body.putString("path", path); + body.putString("message", error.getMessage()); + + WritableMap evt = Arguments.createMap(); + evt.putString("eventName", STORAGE_ERROR); + evt.putMap("body", body); + + Utils.sendEvent(this.getReactApplicationContext(), STORAGE_ERROR, evt); + } + + private WritableMap makeErrorPayload(double code, Exception ex) { + WritableMap error = Arguments.createMap(); + error.putDouble("code", code); + error.putString("message", ex.getMessage()); + return error; + } + + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + + constants.put(DocumentDirectory, 0); + constants.put(DocumentDirectoryPath, this.getReactApplicationContext().getFilesDir().getAbsolutePath()); + constants.put(TemporaryDirectoryPath, null); + constants.put(PicturesDirectoryPath, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); + constants.put(CachesDirectoryPath, this.getReactApplicationContext().getCacheDir().getAbsolutePath()); + constants.put(FileTypeRegular, 0); + constants.put(FileTypeDirectory, 1); + + File externalStorageDirectory = Environment.getExternalStorageDirectory(); + if (externalStorageDirectory != null) { + constants.put(ExternalStorageDirectoryPath, externalStorageDirectory.getAbsolutePath()); + } else { + constants.put(ExternalStorageDirectoryPath, null); + } + + File externalDirectory = this.getReactApplicationContext().getExternalFilesDir(null); + if (externalDirectory != null) { + constants.put(ExternalDirectoryPath, externalDirectory.getAbsolutePath()); + } else { + constants.put(ExternalDirectoryPath, null); + } + + return constants; + } +}