diff --git a/android/src/main/java/io/fullstack/firestack/FirestackMessagingService.java b/android/src/main/java/io/fullstack/firestack/FirestackMessagingService.java index 07ae241..9c32564 100644 --- a/android/src/main/java/io/fullstack/firestack/FirestackMessagingService.java +++ b/android/src/main/java/io/fullstack/firestack/FirestackMessagingService.java @@ -18,15 +18,15 @@ 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()); } - if (remoteMessage.getNotification() != null) { - } Intent i = new Intent(FirestackMessaging.INTENT_NAME_NOTIFICATION); i.putExtra("data", remoteMessage); sendOrderedBroadcast(i, null); diff --git a/android/src/main/java/io/fullstack/firestack/FirestackModule.java b/android/src/main/java/io/fullstack/firestack/FirestackModule.java index 53f95f5..d537411 100644 --- a/android/src/main/java/io/fullstack/firestack/FirestackModule.java +++ b/android/src/main/java/io/fullstack/firestack/FirestackModule.java @@ -1,22 +1,20 @@ package io.fullstack.firestack; -import java.util.Date; -import java.util.HashMap; 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.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.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; @@ -32,16 +30,10 @@ interface KeySetterFn { @SuppressWarnings("WeakerAccess") public class FirestackModule extends ReactContextBaseJavaModule implements LifecycleEventListener { private static final String TAG = "Firestack"; - private Context context; - private ReactContext mReactContext; private FirebaseApp app; - public FirestackModule(ReactApplicationContext reactContext, Context context) { + public FirestackModule(ReactApplicationContext reactContext) { super(reactContext); - this.context = context; - mReactContext = reactContext; - - Log.d(TAG, "New instance"); } @Override @@ -69,7 +61,7 @@ public void configureWithOptions(final ReadableMap params, @Nullable final Callb Log.i(TAG, "configureWithOptions"); FirebaseOptions.Builder builder = new FirebaseOptions.Builder(); - FirebaseOptions defaultOptions = FirebaseOptions.fromResource(this.context); + FirebaseOptions defaultOptions = FirebaseOptions.fromResource(getReactApplicationContext().getBaseContext()); if (defaultOptions == null) { defaultOptions = new FirebaseOptions.Builder().build(); @@ -154,7 +146,7 @@ public String setKeyOrDefault( try { Log.i(TAG, "Configuring app"); if (app == null) { - app = FirebaseApp.initializeApp(this.context, builder.build()); + app = FirebaseApp.initializeApp(getReactApplicationContext().getBaseContext(), builder.build()); } Log.i(TAG, "Configured"); @@ -189,14 +181,14 @@ public void serverValue(@Nullable final Callback onComplete) { public void onHostResume() { WritableMap params = Arguments.createMap(); params.putBoolean("isForground", true); - Utils.sendEvent(mReactContext, "FirestackAppState", params); + Utils.sendEvent(getReactApplicationContext(), "FirestackAppState", params); } @Override public void onHostPause() { WritableMap params = Arguments.createMap(); params.putBoolean("isForground", false); - Utils.sendEvent(mReactContext, "FirestackAppState", params); + Utils.sendEvent(getReactApplicationContext(), "FirestackAppState", params); } @Override @@ -208,6 +200,8 @@ public void onHostDestroy() { 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/fullstack/firestack/FirestackPackage.java b/android/src/main/java/io/fullstack/firestack/FirestackPackage.java index 6a71080..9a22981 100644 --- a/android/src/main/java/io/fullstack/firestack/FirestackPackage.java +++ b/android/src/main/java/io/fullstack/firestack/FirestackPackage.java @@ -32,7 +32,7 @@ public FirestackPackage() { @Override public List createNativeModules(ReactApplicationContext reactContext) { List modules = new ArrayList<>(); - modules.add(new FirestackModule(reactContext, reactContext.getBaseContext())); + modules.add(new FirestackModule(reactContext)); modules.add(new FirestackAuth(reactContext)); modules.add(new FirestackDatabase(reactContext)); modules.add(new FirestackAnalytics(reactContext)); diff --git a/android/src/main/java/io/fullstack/firestack/Utils.java b/android/src/main/java/io/fullstack/firestack/Utils.java index 24950a8..a763204 100644 --- a/android/src/main/java/io/fullstack/firestack/Utils.java +++ b/android/src/main/java/io/fullstack/firestack/Utils.java @@ -13,6 +13,7 @@ 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; @@ -82,8 +83,12 @@ public static WritableMap dataSnapshotToMap( data.putString("value", null); } } else { - WritableMap valueMap = Utils.castSnapshotValue(dataSnapshot); - data.putMap("value", valueMap); + Object value = Utils.castSnapshotValue(dataSnapshot); + if (value instanceof WritableNativeArray) { + data.putArray("value", (WritableArray) value); + } else { + data.putMap("value", (WritableMap) value); + } } // Child keys diff --git a/android/src/main/java/io/fullstack/firestack/analytics/FirestackAnalytics.java b/android/src/main/java/io/fullstack/firestack/analytics/FirestackAnalytics.java index fc85ba8..14e6ac2 100644 --- a/android/src/main/java/io/fullstack/firestack/analytics/FirestackAnalytics.java +++ b/android/src/main/java/io/fullstack/firestack/analytics/FirestackAnalytics.java @@ -1,30 +1,24 @@ package io.fullstack.firestack.analytics; -import java.util.Map; import android.util.Log; -import android.os.Bundle; 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; -import io.fullstack.firestack.Utils; public class FirestackAnalytics extends ReactContextBaseJavaModule { private static final String TAG = "FirestackAnalytics"; - private ReactApplicationContext context; - private FirebaseAnalytics mFirebaseAnalytics; - public FirestackAnalytics(ReactApplicationContext reactContext) { super(reactContext); - context = reactContext; Log.d(TAG, "New instance"); - mFirebaseAnalytics = FirebaseAnalytics.getInstance(this.context); } /** @@ -37,11 +31,8 @@ public String getName() { } @ReactMethod - public void logEvent(final String name, final ReadableMap params) { - Map m = Utils.recursivelyDeconstructReadableMap(params); - final Bundle bundle = makeEventBundle(name, m); - Log.d(TAG, "Logging event " + name); - mFirebaseAnalytics.logEvent(name, bundle); + public void logEvent(final String name, @Nullable final ReadableMap params) { + FirebaseAnalytics.getInstance(getReactApplicationContext()).logEvent(name, Arguments.toBundle(params)); } /** @@ -50,7 +41,7 @@ public void logEvent(final String name, final ReadableMap params) { */ @ReactMethod public void setAnalyticsCollectionEnabled(final Boolean enabled) { - mFirebaseAnalytics.setAnalyticsCollectionEnabled(enabled); + FirebaseAnalytics.getInstance(getReactApplicationContext()).setAnalyticsCollectionEnabled(enabled); } /** @@ -62,12 +53,12 @@ public void setAnalyticsCollectionEnabled(final Boolean enabled) { public void setCurrentScreen(final String screenName, final String screenClassOverride) { final Activity activity = getCurrentActivity(); if (activity != null) { - Log.d(TAG, "setCurrentScreen " + screenName + " - " + screenClassOverride); // needs to be run on main thread + Log.d(TAG, "setCurrentScreen " + screenName + " - " + screenClassOverride); activity.runOnUiThread(new Runnable() { @Override public void run() { - mFirebaseAnalytics.setCurrentScreen(activity, screenName, screenClassOverride); + FirebaseAnalytics.getInstance(getReactApplicationContext()).setCurrentScreen(activity, screenName, screenClassOverride); } }); } @@ -79,7 +70,7 @@ public void run() { */ @ReactMethod public void setMinimumSessionDuration(final double milliseconds) { - mFirebaseAnalytics.setMinimumSessionDuration((long) milliseconds); + FirebaseAnalytics.getInstance(getReactApplicationContext()).setMinimumSessionDuration((long) milliseconds); } /** @@ -88,7 +79,7 @@ public void setMinimumSessionDuration(final double milliseconds) { */ @ReactMethod public void setSessionTimeoutDuration(final double milliseconds) { - mFirebaseAnalytics.setSessionTimeoutDuration((long) milliseconds); + FirebaseAnalytics.getInstance(getReactApplicationContext()).setSessionTimeoutDuration((long) milliseconds); } /** @@ -97,7 +88,7 @@ public void setSessionTimeoutDuration(final double milliseconds) { */ @ReactMethod public void setUserId(final String id) { - mFirebaseAnalytics.setUserId(id); + FirebaseAnalytics.getInstance(getReactApplicationContext()).setUserId(id); } /** @@ -107,144 +98,6 @@ public void setUserId(final String id) { */ @ReactMethod public void setUserProperty(final String name, final String value) { - mFirebaseAnalytics.setUserProperty(name, value); - } - - // todo refactor/clean me - private Bundle makeEventBundle(final String name, final Map map) { - Bundle bundle = new Bundle(); - // Available from the FirestackAnalytics event - if (map.containsKey("id")) { - String id = (String) map.get("id"); - bundle.putString(FirebaseAnalytics.Param.ITEM_ID, id); - } - if (map.containsKey("name")) { - String val = (String) map.get("name"); - bundle.putString(FirebaseAnalytics.Param.ITEM_NAME, val); - } - if (map.containsKey("category")) { - String val = (String) map.get("category"); - bundle.putString(FirebaseAnalytics.Param.ITEM_NAME, val); - } - if (map.containsKey("quantity")) { - double val = (double) map.get("quantity"); - bundle.putDouble(FirebaseAnalytics.Param.QUANTITY, val); - } - if (map.containsKey("price")) { - double val = (double) map.get("price"); - bundle.putDouble(FirebaseAnalytics.Param.PRICE, val); - } - if (map.containsKey("value")) { - double val = (double) map.get("value"); - bundle.putDouble(FirebaseAnalytics.Param.VALUE, val); - } - if (map.containsKey("currency")) { - String val = (String) map.get("currency"); - bundle.putString(FirebaseAnalytics.Param.CURRENCY, val); - } - if (map.containsKey("origin")) { - String val = (String) map.get("origin"); - bundle.putString(FirebaseAnalytics.Param.ORIGIN, val); - } - if (map.containsKey("item_location_id")) { - String val = (String) map.get("item_location_id"); - bundle.putString(FirebaseAnalytics.Param.ITEM_LOCATION_ID, val); - } - if (map.containsKey("location")) { - String val = (String) map.get("location"); - bundle.putString(FirebaseAnalytics.Param.LOCATION, val); - } - if (map.containsKey("destination")) { - String val = (String) map.get("destination"); - bundle.putString(FirebaseAnalytics.Param.DESTINATION, val); - } - if (map.containsKey("start_date")) { - String val = (String) map.get("start_date"); - bundle.putString(FirebaseAnalytics.Param.START_DATE, val); - } - if (map.containsKey("end_date")) { - String val = (String) map.get("end_date"); - bundle.putString(FirebaseAnalytics.Param.END_DATE, val); - } - if (map.containsKey("transaction_id")) { - String val = (String) map.get("transaction_id"); - bundle.putString(FirebaseAnalytics.Param.TRANSACTION_ID, val); - } - if (map.containsKey("number_of_nights")) { - long val = (long) map.get("number_of_nights"); - bundle.putLong(FirebaseAnalytics.Param.NUMBER_OF_NIGHTS, val); - } - if (map.containsKey("number_of_rooms")) { - long val = (long) map.get("number_of_rooms"); - bundle.putLong(FirebaseAnalytics.Param.NUMBER_OF_ROOMS, val); - } - if (map.containsKey("number_of_passengers")) { - long val = (long) map.get("number_of_passengers"); - bundle.putLong(FirebaseAnalytics.Param.NUMBER_OF_PASSENGERS, val); - } - if (map.containsKey("travel_class")) { - String val = (String) map.get("travel_class"); - bundle.putString(FirebaseAnalytics.Param.TRAVEL_CLASS, val); - } - if (map.containsKey("coupon")) { - String val = (String) map.get("coupon"); - bundle.putString(FirebaseAnalytics.Param.COUPON, val); - } - if (map.containsKey("tax")) { - long val = (long) map.get("tax"); - bundle.putLong(FirebaseAnalytics.Param.TAX, val); - } - if (map.containsKey("shipping")) { - double val = (double) map.get("shipping"); - bundle.putDouble(FirebaseAnalytics.Param.SHIPPING, val); - } - if (map.containsKey("group_id")) { - String val = (String) map.get("group_id"); - bundle.putString(FirebaseAnalytics.Param.GROUP_ID, val); - } - if (map.containsKey("level")) { - long val = (long) map.get("level"); - bundle.putLong(FirebaseAnalytics.Param.LEVEL, val); - } - if (map.containsKey("character")) { - String val = (String) map.get("character"); - bundle.putString(FirebaseAnalytics.Param.CHARACTER, val); - } - if (map.containsKey("score")) { - long val = (long) map.get("score"); - bundle.putLong(FirebaseAnalytics.Param.SCORE, val); - } - if (map.containsKey("search_term")) { - String val = (String) map.get("search_term"); - bundle.putString(FirebaseAnalytics.Param.SEARCH_TERM, val); - } - if (map.containsKey("content_type")) { - String val = (String) map.get("content_type"); - bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, val); - } - if (map.containsKey("sign_up_method")) { - String val = (String) map.get("sign_up_method"); - bundle.putString(FirebaseAnalytics.Param.SIGN_UP_METHOD, val); - } - if (map.containsKey("virtual_currency_name")) { - String val = (String) map.get("virtual_currency_name"); - bundle.putString(FirebaseAnalytics.Param.VIRTUAL_CURRENCY_NAME, val); - } - if (map.containsKey("achievement_id")) { - String val = (String) map.get("achievement_id"); - bundle.putString(FirebaseAnalytics.Param.ACHIEVEMENT_ID, val); - } - if (map.containsKey("flight_number")) { - String val = (String) map.get("flight_number"); - bundle.putString(FirebaseAnalytics.Param.FLIGHT_NUMBER, val); - } - - for (Map.Entry entry : map.entrySet()) { - if (bundle.getBundle(entry.getKey()) == null) { - bundle.putString(entry.getKey(), entry.getValue().toString()); - } - } - - return bundle; + FirebaseAnalytics.getInstance(getReactApplicationContext()).setUserProperty(name, value); } } diff --git a/android/src/main/java/io/fullstack/firestack/database/FirestackDatabase.java b/android/src/main/java/io/fullstack/firestack/database/FirestackDatabase.java index c7e1a76..971d2da 100644 --- a/android/src/main/java/io/fullstack/firestack/database/FirestackDatabase.java +++ b/android/src/main/java/io/fullstack/firestack/database/FirestackDatabase.java @@ -20,6 +20,7 @@ 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.fullstack.firestack.Utils; @@ -170,14 +171,14 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { public void on(final String path, final String modifiersString, final ReadableArray modifiersArray, - final String name, + final String eventName, final Callback callback) { FirestackDatabaseReference ref = this.getDBHandle(path, modifiersArray, modifiersString); - if (name.equals("value")) { + if (eventName.equals("value")) { ref.addValueEventListener(); } else { - ref.addChildEventListener(name); + ref.addChildEventListener(eventName); } WritableMap resp = Arguments.createMap(); @@ -190,7 +191,7 @@ public void on(final String path, public void onOnce(final String path, final String modifiersString, final ReadableArray modifiersArray, - final String name, + final String eventName, final Callback callback) { FirestackDatabaseReference ref = this.getDBHandle(path, modifiersArray, modifiersString); ref.addOnceValueEventListener(callback); @@ -206,22 +207,21 @@ public void onOnce(final String path, public void off( final String path, final String modifiersString, - final String name, + final String eventName, final Callback callback) { String key = this.getDBListenerKey(path, modifiersString); FirestackDatabaseReference r = mDBListeners.get(key); if (r != null) { - if (name == null || "".equals(name)) { + if (eventName == null || "".equals(eventName)) { r.cleanup(); mDBListeners.remove(key); } else { - //TODO: Remove individual listeners as per iOS code - //1) Remove event handler - //2) If no more listeners, remove from listeners map - r.cleanup(); - mDBListeners.remove(key); + r.removeEventListener(eventName); + if (!r.hasListeners()) { + mDBListeners.remove(key); + } ; } } @@ -333,4 +333,11 @@ private FirestackDatabaseReference getDBHandle(final String path, 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/fullstack/firestack/database/FirestackDatabaseReference.java b/android/src/main/java/io/fullstack/firestack/database/FirestackDatabaseReference.java index 6f77a76..223cf0a 100644 --- a/android/src/main/java/io/fullstack/firestack/database/FirestackDatabaseReference.java +++ b/android/src/main/java/io/fullstack/firestack/database/FirestackDatabaseReference.java @@ -1,8 +1,10 @@ package io.fullstack.firestack.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; @@ -28,6 +30,7 @@ public class FirestackDatabaseReference { private ChildEventListener mEventListener; private ValueEventListener mValueListener; private ReactContext mReactContext; + private Set childEventListeners = new HashSet<>(); public FirestackDatabaseReference(final ReactContext context, final FirebaseDatabase firebaseDatabase, @@ -40,7 +43,7 @@ public FirestackDatabaseReference(final ReactContext context, mQuery = this.buildDatabaseQueryAtPathAndModifiers(firebaseDatabase, path, modifiersArray); } - public void addChildEventListener(final String name) { + public void addChildEventListener(final String eventName) { if (mEventListener == null) { mEventListener = new ChildEventListener() { @Override @@ -65,7 +68,7 @@ public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { @Override public void onCancelled(DatabaseError error) { - handleDatabaseError(name, error); + handleDatabaseError(eventName, error); } }; mQuery.addChildEventListener(mEventListener); @@ -73,6 +76,8 @@ public void onCancelled(DatabaseError error) { } 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() { @@ -117,8 +122,24 @@ public void onCancelled(DatabaseError error) { 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(); } @@ -272,5 +293,4 @@ private Query buildDatabaseQueryAtPathAndModifiers(final FirebaseDatabase fireba return query; } - } diff --git a/android/src/main/java/io/fullstack/firestack/messaging/FirestackMessaging.java b/android/src/main/java/io/fullstack/firestack/messaging/FirestackMessaging.java index d4e2ff9..7ef06d2 100644 --- a/android/src/main/java/io/fullstack/firestack/messaging/FirestackMessaging.java +++ b/android/src/main/java/io/fullstack/firestack/messaging/FirestackMessaging.java @@ -27,177 +27,192 @@ public class FirestackMessaging extends ReactContextBaseJavaModule { - private static final String TAG = "FirestackMessaging"; - private static final String EVENT_NAME_TOKEN = "FirestackRefreshToken"; - private static final String EVENT_NAME_NOTIFICATION = "FirestackReceiveNotification"; - private static final String EVENT_NAME_SEND = "FirestackUpstreamSend"; - - public static final String INTENT_NAME_TOKEN = "io.fullstack.firestack.refreshToken"; - public static final String INTENT_NAME_NOTIFICATION = "io.fullstack.firestack.ReceiveNotification"; - public static final String INTENT_NAME_SEND = "io.fullstack.firestack.Upstream"; - - private IntentFilter mRefreshTokenIntentFilter; - private IntentFilter mReceiveNotificationIntentFilter; - private IntentFilter mReceiveSendIntentFilter; - - public FirestackMessaging(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; - } - - @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); - } - } - - /** - * - */ - 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); + private static final String TAG = "FirestackMessaging"; + private static final String EVENT_NAME_TOKEN = "FirestackRefreshToken"; + private static final String EVENT_NAME_NOTIFICATION = "FirestackReceiveNotification"; + private static final String EVENT_NAME_SEND = "FirestackUpstreamSend"; + + public static final String INTENT_NAME_TOKEN = "io.fullstack.firestack.refreshToken"; + public static final String INTENT_NAME_NOTIFICATION = "io.fullstack.firestack.ReceiveNotification"; + public static final String INTENT_NAME_SEND = "io.fullstack.firestack.Upstream"; + + private IntentFilter mRefreshTokenIntentFilter; + private IntentFilter mReceiveNotificationIntentFilter; + private IntentFilter mReceiveSendIntentFilter; + private BroadcastReceiver mBroadcastReceiver; + + public FirestackMessaging(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, "Firestack 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)); } - ; - }, mRefreshTokenIntentFilter); - } + params.putMap("data", dataMap); + } - @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); - } - } + 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); + } - @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); + ReactContext ctx = getReactApplicationContext(); + Utils.sendEvent(ctx, EVENT_NAME_NOTIFICATION, params); } - } + }; - private void initMessageHandler() { - Log.d(TAG, "Firestack initMessageHandler called"); - getReactApplicationContext().registerReceiver(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(); - if (remoteMessage.getData().size() != 0) { - WritableMap dataMap = Arguments.createMap(); - Map data = remoteMessage.getData(); - //Set keysIterator = data.keySet(); - for (String key : data.keySet()) { - dataMap.putString(key, data.get(key)); - } - params.putMap("data", dataMap); - } else { - params.putNull("data"); - } - 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); - } else { - params.putNull("notification"); - } - ReactContext ctx = getReactApplicationContext(); - Utils.sendEvent(ctx, EVENT_NAME_NOTIFICATION, params); - } - }, mReceiveNotificationIntentFilter); } + 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 send(String senderId, String messageId, String messageType, ReadableMap params, final Callback callback) { - FirebaseMessaging fm = FirebaseMessaging.getInstance(); - RemoteMessage.Builder remoteMessage = new RemoteMessage.Builder(senderId); - remoteMessage.setMessageId(messageId); - remoteMessage.setMessageType(messageType); - ReadableMapKeySetIterator iterator = params.keySetIterator(); - while (iterator.hasNextKey()) { - String key = iterator.nextKey(); - ReadableType type = params.getType(key); - if (type == ReadableType.String) { - remoteMessage.addData(key, params.getString(key)); - Log.d(TAG, "Firebase send: " + key); - Log.d(TAG, "Firebase send: " + params.getString(key)); - } - } - try { - fm.send(remoteMessage.build()); - WritableMap res = Arguments.createMap(); - res.putString("status", "success"); - callback.invoke(null, res); - } catch(Exception e) { - Log.e(TAG, "Error sending message", e); - WritableMap error = Arguments.createMap(); - error.putString("code", e.toString()); - error.putString("message", e.toString()); - 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); + } + } + + @ReactMethod + public void send(String senderId, String messageId, String messageType, ReadableMap params, final Callback callback) { + FirebaseMessaging fm = FirebaseMessaging.getInstance(); + RemoteMessage.Builder remoteMessage = new RemoteMessage.Builder(senderId); + remoteMessage.setMessageId(messageId); + remoteMessage.setMessageType(messageType); + ReadableMapKeySetIterator iterator = params.keySetIterator(); + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = params.getType(key); + if (type == ReadableType.String) { + remoteMessage.addData(key, params.getString(key)); + Log.d(TAG, "Firebase send: " + key); + Log.d(TAG, "Firebase send: " + params.getString(key)); + } } - 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); + try { + fm.send(remoteMessage.build()); + WritableMap res = Arguments.createMap(); + res.putString("status", "success"); + callback.invoke(null, res); + } catch (Exception e) { + Log.e(TAG, "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/fullstack/firestack/storage/FirestackStorage.java b/android/src/main/java/io/fullstack/firestack/storage/FirestackStorage.java index 19b23d7..cae695f 100644 --- a/android/src/main/java/io/fullstack/firestack/storage/FirestackStorage.java +++ b/android/src/main/java/io/fullstack/firestack/storage/FirestackStorage.java @@ -17,6 +17,7 @@ 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; @@ -28,7 +29,10 @@ 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; @@ -55,13 +59,11 @@ public class FirestackStorage extends ReactContextBaseJavaModule { private static final String FileTypeRegular = "FILETYPE_REGULAR"; private static final String FileTypeDirectory = "FILETYPE_DIRECTORY"; - private static final String STORAGE_UPLOAD_PROGRESS = "upload_progress"; - private static final String STORAGE_UPLOAD_PAUSED = "upload_paused"; - private static final String STORAGE_UPLOAD_RESUMED = "upload_resumed"; - - private static final String STORAGE_DOWNLOAD_PROGRESS = "download_progress"; - private static final String STORAGE_DOWNLOAD_PAUSED = "download_paused"; - private static final String STORAGE_DOWNLOAD_RESUMED = "download_resumed"; + 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"; @@ -85,8 +87,88 @@ public boolean isExternalStorageWritable() { } @ReactMethod - public void downloadFile(final String remotePath, - final String localFile, + 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"); @@ -97,16 +179,16 @@ public void downloadFile(final String remotePath, callback.invoke(error); return; } - Log.d(TAG, "downloadFile from remote path: " + remotePath); + Log.d(TAG, "downloadFile from remote path: " + path); - StorageReference fileRef = FirebaseStorage.getInstance().getReference(remotePath); + StorageReference reference = this.getReference(path); - fileRef.getStream(new StreamDownloadTask.StreamProcessor() { + reference.getStream(new StreamDownloadTask.StreamProcessor() { @Override public void doInBackground(StreamDownloadTask.TaskSnapshot taskSnapshot, InputStream inputStream) throws IOException { - int indexOfLastSlash = localFile.lastIndexOf("/"); - String pathMinusFileName = indexOfLastSlash>0 ? localFile.substring(0, indexOfLastSlash) + "/" : "/"; - String filename = indexOfLastSlash>0 ? localFile.substring(indexOfLastSlash+1) : localFile; + 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); @@ -122,152 +204,54 @@ public void doInBackground(StreamDownloadTask.TaskSnapshot taskSnapshot, InputSt }).addOnProgressListener(new OnProgressListener() { @Override public void onProgress(StreamDownloadTask.TaskSnapshot taskSnapshot) { - WritableMap data = Arguments.createMap(); - data.putString("ref", taskSnapshot.getStorage().getBucket()); - double percentComplete = taskSnapshot.getTotalByteCount() == 0 ? 0.0f : 100.0f * (taskSnapshot.getBytesTransferred()) / (taskSnapshot.getTotalByteCount()); - data.putDouble("progress", percentComplete); - Utils.sendEvent(mReactContext, STORAGE_DOWNLOAD_PROGRESS, data); + 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) { - WritableMap data = Arguments.createMap(); - data.putString("ref", taskSnapshot.getStorage().getBucket()); - Utils.sendEvent(mReactContext, STORAGE_DOWNLOAD_PAUSED, data); + 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) { - final WritableMap data = Arguments.createMap(); - StorageReference ref = taskSnapshot.getStorage(); - data.putString("fullPath", ref.getPath()); - data.putString("bucket", ref.getBucket()); - data.putString("name", ref.getName()); - ref.getMetadata().addOnSuccessListener(new OnSuccessListener() { - @Override - public void onSuccess(final StorageMetadata storageMetadata) { - data.putMap("metadata", getMetadataAsMap(storageMetadata)); - callback.invoke(null, data); - } - }) - .addOnFailureListener(new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception exception) { - final int errorCode = 1; - WritableMap data = Arguments.createMap(); - StorageException storageException = StorageException.fromException(exception); - data.putString("description", storageException.getMessage()); - data.putInt("code", errorCode); - callback.invoke(makeErrorPayload(errorCode, exception)); - } - }); + 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) { - final int errorCode = 1; - WritableMap data = Arguments.createMap(); - StorageException storageException = StorageException.fromException(exception); - data.putString("description", storageException.getMessage()); - data.putInt("code", errorCode); - callback.invoke(makeErrorPayload(errorCode, exception)); + Log.e(TAG, "Failed to download file " + exception.getMessage()); + //TODO: JS Error event + callback.invoke(makeErrorPayload(1, exception)); } }); } @ReactMethod - public void downloadUrl(final String remotePath, - final Callback callback) { - Log.d(TAG, "Download url for remote path: " + remotePath); - final StorageReference fileRef = FirebaseStorage.getInstance().getReference(remotePath); - - Task downloadTask = fileRef.getDownloadUrl(); - downloadTask - .addOnSuccessListener(new OnSuccessListener() { - @Override - public void onSuccess(Uri uri) { - final WritableMap res = Arguments.createMap(); - - res.putString("status", "success"); - res.putString("bucket", FirebaseStorage.getInstance().getApp().getOptions().getStorageBucket()); - res.putString("fullPath", uri.toString()); - res.putString("path", uri.getPath()); - res.putString("url", uri.toString()); - - fileRef.getMetadata() - .addOnSuccessListener(new OnSuccessListener() { - @Override - public void onSuccess(final StorageMetadata storageMetadata) { - Log.d(TAG, "getMetadata success " + storageMetadata); - - res.putMap("metadata", getMetadataAsMap(storageMetadata)); - res.putString("name", storageMetadata.getName()); - res.putString("url", storageMetadata.getDownloadUrl().toString()); - callback.invoke(null, res); - } - }) - .addOnFailureListener(new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception exception) { - Log.e(TAG, "Failure in download " + exception); - final int errorCode = 1; - callback.invoke(makeErrorPayload(errorCode, exception)); - } - }); - - } - }) - .addOnFailureListener(new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception exception) { - Log.e(TAG, "Failed to download file " + exception.getMessage()); - - WritableMap err = Arguments.createMap(); - err.putString("status", "error"); - err.putString("description", exception.getLocalizedMessage()); - - callback.invoke(err); - } - }); - } - - private WritableMap getMetadataAsMap(StorageMetadata storageMetadata) { - WritableMap metadata = Arguments.createMap(); - metadata.putString("getBucket", storageMetadata.getBucket()); - metadata.putString("getName", storageMetadata.getName()); - metadata.putDouble("sizeBytes", storageMetadata.getSizeBytes()); - metadata.putDouble("created_at", storageMetadata.getCreationTimeMillis()); - metadata.putDouble("updated_at", storageMetadata.getUpdatedTimeMillis()); - metadata.putString("md5hash", storageMetadata.getMd5Hash()); - metadata.putString("encoding", storageMetadata.getContentEncoding()); - return metadata; - } - - // STORAGE - @ReactMethod - public void uploadFile(final String remotePath, final String filepath, final ReadableMap metadata, final Callback callback) { - StorageReference fileRef = FirebaseStorage.getInstance().getReference(remotePath); + 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: " + filepath + " to " + remotePath); + Log.i(TAG, "Upload file: " + localPath + " to " + path); try { Uri file; - if (filepath.startsWith("content://")) { - String realPath = getRealPathFromURI(filepath); + if (localPath.startsWith("content://")) { + String realPath = getRealPathFromURI(localPath); file = Uri.fromFile(new File(realPath)); } else { - file = Uri.fromFile(new File(filepath)); + file = Uri.fromFile(new File(localPath)); } - StorageMetadata.Builder metadataBuilder = new StorageMetadata.Builder(); - Map m = Utils.recursivelyDeconstructReadableMap(metadata); - - for (Map.Entry entry : m.entrySet()) { - metadataBuilder.setCustomMetadata(entry.getKey(), entry.getValue().toString()); - } - - StorageMetadata md = metadataBuilder.build(); - UploadTask uploadTask = fileRef.putFile(file, md); + 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 @@ -276,49 +260,35 @@ public void uploadFile(final String remotePath, final String filepath, final Rea public void onFailure(@NonNull Exception exception) { // handle unsuccessful uploads Log.e(TAG, "Failed to upload file " + exception.getMessage()); - - WritableMap err = Arguments.createMap(); - err.putString("description", exception.getLocalizedMessage()); - - callback.invoke(err); + //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); - // taskSnapshot.getMetadata() contains file metadata such as size, content-type, and download URL. - WritableMap resp = getDownloadData(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) { - double totalBytes = taskSnapshot.getTotalByteCount(); - double bytesTransferred = taskSnapshot.getBytesTransferred(); - double progress = (100.0 * bytesTransferred) / totalBytes; - - System.out.println("Transferred " + bytesTransferred + "/" + totalBytes + "(" + progress + "% complete)"); - - if (progress >= 0) { - WritableMap data = Arguments.createMap(); - data.putString("eventName", STORAGE_UPLOAD_PROGRESS); - data.putDouble("progress", progress); - Utils.sendEvent(getReactApplicationContext(), STORAGE_UPLOAD_PROGRESS, data); - } + 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) { - System.out.println("Upload is paused"); - StorageMetadata d = taskSnapshot.getMetadata(); - String bucket = d.getBucket(); - WritableMap data = Arguments.createMap(); - data.putString("eventName", STORAGE_UPLOAD_PAUSED); - data.putString("ref", bucket); - Utils.sendEvent(getReactApplicationContext(), STORAGE_UPLOAD_PAUSED, data); + Log.d(TAG, "Upload is paused " + taskSnapshot); + WritableMap event = getUploadTaskAsMap(taskSnapshot); + handleStorageEvent(STORAGE_STATE_CHANGED, path, event); } }); } catch (Exception ex) { @@ -327,18 +297,74 @@ public void onPaused(UploadTask.TaskSnapshot taskSnapshot) { } } + //Firebase.Storage methods @ReactMethod - public void getRealPathFromURI(final String uri, final Callback callback) { - try { - String path = getRealPathFromURI(uri); - callback.invoke(null, path); - } catch (Exception ex) { - ex.printStackTrace(); - final int errorCode = 1; - callback.invoke(makeErrorPayload(errorCode, ex)); + 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 { @@ -354,25 +380,71 @@ private String getRealPathFromURI(final String uri) { } } - private WritableMap getDownloadData(final UploadTask.TaskSnapshot taskSnapshot) { - Uri downloadUrl = taskSnapshot.getDownloadUrl(); + 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.putString("downloadUrl", downloadUrl.toString()); - resp.putString("fullPath", d.getPath()); - resp.putString("bucket", d.getBucket()); - resp.putString("name", d.getName()); - - WritableMap metadataObj = Arguments.createMap(); - metadataObj.putString("cacheControl", d.getCacheControl()); - metadataObj.putString("contentDisposition", d.getContentDisposition()); - metadataObj.putString("contentType", d.getContentType()); - resp.putMap("metadata", metadataObj); + 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); diff --git a/ios/Firestack/FirestackEvents.h b/ios/Firestack/FirestackEvents.h index ba976ef..fd6b01d 100644 --- a/ios/Firestack/FirestackEvents.h +++ b/ios/Firestack/FirestackEvents.h @@ -37,12 +37,14 @@ static NSString *const DATABASE_CHILD_REMOVED_EVENT = @"child_removed"; static NSString *const DATABASE_CHILD_MOVED_EVENT = @"child_moved"; // Storage -static NSString *const STORAGE_UPLOAD_PROGRESS = @"upload_progress"; -static NSString *const STORAGE_UPLOAD_PAUSED = @"upload_paused"; -static NSString *const STORAGE_UPLOAD_RESUMED = @"upload_resumed"; -static NSString *const STORAGE_DOWNLOAD_PROGRESS = @"download_progress"; -static NSString *const STORAGE_DOWNLOAD_PAUSED = @"download_paused"; -static NSString *const STORAGE_DOWNLOAD_RESUMED = @"download_resumed"; +static NSString *const STORAGE_EVENT = @"storage_event"; +static NSString *const STORAGE_ERROR = @"storage_error"; + +static NSString *const STORAGE_STATE_CHANGED = @"state_changed"; +static NSString *const STORAGE_UPLOAD_SUCCESS = @"upload_success"; +static NSString *const STORAGE_UPLOAD_FAILURE = @"upload_failure"; +static NSString *const STORAGE_DOWNLOAD_SUCCESS = @"download_success"; +static NSString *const STORAGE_DOWNLOAD_FAILURE = @"download_failure"; // Messaging static NSString *const MESSAGING_SUBSYSTEM_EVENT = @"messaging_event"; diff --git a/ios/Firestack/FirestackStorage.m b/ios/Firestack/FirestackStorage.m index cfcf2fa..b2e9cd8 100644 --- a/ios/Firestack/FirestackStorage.m +++ b/ios/Firestack/FirestackStorage.m @@ -21,109 +21,213 @@ - (dispatch_queue_t)methodQueue return dispatch_queue_create("io.fullstack.firestack.storage", DISPATCH_QUEUE_SERIAL); } -RCT_EXPORT_METHOD(downloadUrl: (NSString *) remotePath - callback:(RCTResponseSenderBlock) callback) +RCT_EXPORT_METHOD(delete: (NSString *) path + callback:(RCTResponseSenderBlock) callback) +{ + + FIRStorageReference *fileRef = [self getReference:path]; + [fileRef deleteWithCompletion:^(NSError * _Nullable error) { + if (error == nil) { + NSDictionary *resp = @{ + @"status": @"success", + @"path": path + }; + callback(@[[NSNull null], resp]); + } else { + NSDictionary *evt = @{ + @"status": @"error", + @"path": path, + @"message": [error debugDescription] + }; + callback(@[evt]); + } + }]; +} + +RCT_EXPORT_METHOD(getDownloadURL: (NSString *) path + callback:(RCTResponseSenderBlock) callback) { - FIRStorageReference *fileRef = [[FIRStorage storage] referenceWithPath:remotePath]; + FIRStorageReference *fileRef = [self getReference:path]; [fileRef downloadURLWithCompletion:^(NSURL * _Nullable URL, NSError * _Nullable error) { if (error != nil) { NSDictionary *evt = @{ @"status": @"error", - @"path": remotePath, - @"msg": [error debugDescription] + @"path": path, + @"message": [error debugDescription] }; callback(@[evt]); } else { - NSDictionary *resp = @{ - @"status": @"success", - @"url": [URL absoluteString], - @"path": [URL path] - }; + callback(@[[NSNull null], [URL absoluteString]]); + } + }]; +} + +RCT_EXPORT_METHOD(getMetadata: (NSString *) path + callback:(RCTResponseSenderBlock) callback) +{ + FIRStorageReference *fileRef = [self getReference:path]; + [fileRef metadataWithCompletion:^(FIRStorageMetadata * _Nullable metadata, NSError * _Nullable error) { + if (error != nil) { + NSDictionary *evt = @{ + @"status": @"error", + @"path": path, + @"message": [error debugDescription] + }; + callback(@[evt]); + } else { + NSDictionary *resp = [metadata dictionaryRepresentation]; + callback(@[[NSNull null], resp]); + } + }]; +} + +RCT_EXPORT_METHOD(updateMetadata: (NSString *) path + metadata:(NSDictionary *) metadata + callback:(RCTResponseSenderBlock) callback) +{ + FIRStorageReference *fileRef = [self getReference:path]; + FIRStorageMetadata *firmetadata = [[FIRStorageMetadata alloc] initWithDictionary:metadata]; + [fileRef updateMetadata:firmetadata completion:^(FIRStorageMetadata * _Nullable metadata, NSError * _Nullable error) { + if (error != nil) { + NSDictionary *evt = @{ + @"status": @"error", + @"path": path, + @"message": [error debugDescription] + }; + callback(@[evt]); + } else { + NSDictionary *resp = [metadata dictionaryRepresentation]; callback(@[[NSNull null], resp]); } }]; } -RCT_EXPORT_METHOD(uploadFile: (NSString *) remotePath - path:(NSString *)path +RCT_EXPORT_METHOD(downloadFile: (NSString *) path + localPath:(NSString *) localPath + callback:(RCTResponseSenderBlock) callback) +{ + FIRStorageReference *fileRef = [self getReference:path]; + NSURL *localFile = [NSURL fileURLWithPath:localPath]; + + FIRStorageDownloadTask *downloadTask = [fileRef writeToFile:localFile]; + // Listen for state changes, errors, and completion of the download. + [downloadTask observeStatus:FIRStorageTaskStatusResume handler:^(FIRStorageTaskSnapshot *snapshot) { + // Download resumed, also fires when the upload starts + NSDictionary *event = [self getDownloadTaskAsDictionary:snapshot]; + [self sendJSEvent:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event]; + }]; + + [downloadTask observeStatus:FIRStorageTaskStatusPause handler:^(FIRStorageTaskSnapshot *snapshot) { + // Download paused + NSDictionary *event = [self getDownloadTaskAsDictionary:snapshot]; + [self sendJSEvent:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event]; + }]; + [downloadTask observeStatus:FIRStorageTaskStatusProgress handler:^(FIRStorageTaskSnapshot *snapshot) { + // Download reported progress + NSDictionary *event = [self getDownloadTaskAsDictionary:snapshot]; + [self sendJSEvent:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event]; + }]; + + [downloadTask observeStatus:FIRStorageTaskStatusSuccess handler:^(FIRStorageTaskSnapshot *snapshot) { + // Download completed successfully + NSDictionary *resp = [self getDownloadTaskAsDictionary:snapshot]; + + [self sendJSEvent:STORAGE_EVENT path:path title:STORAGE_DOWNLOAD_SUCCESS props:resp]; + callback(@[[NSNull null], resp]); + }]; + + [downloadTask observeStatus:FIRStorageTaskStatusFailure handler:^(FIRStorageTaskSnapshot *snapshot) { + if (snapshot.error != nil) { + NSDictionary *errProps = [[NSMutableDictionary alloc] init]; + NSLog(@"Error in download: %@", snapshot.error); + + switch (snapshot.error.code) { + case FIRStorageErrorCodeObjectNotFound: + // File doesn't exist + [errProps setValue:@"File does not exist" forKey:@"message"]; + break; + case FIRStorageErrorCodeUnauthorized: + // User doesn't have permission to access file + [errProps setValue:@"You do not have permissions" forKey:@"message"]; + break; + case FIRStorageErrorCodeCancelled: + // User canceled the upload + [errProps setValue:@"Download canceled" forKey:@"message"]; + break; + case FIRStorageErrorCodeUnknown: + // Unknown error occurred, inspect the server response + [errProps setValue:@"Unknown error" forKey:@"message"]; + break; + } + + //TODO: Error event + callback(@[errProps]); + }}]; +} + + +RCT_EXPORT_METHOD(putFile:(NSString *) path + localPath:(NSString *)localPath metadata:(NSDictionary *)metadata callback:(RCTResponseSenderBlock) callback) { - FIRStorageReference *uploadRef = [[FIRStorage storage] referenceWithPath:remotePath]; + FIRStorageReference *fileRef = [self getReference:path]; FIRStorageMetadata *firmetadata = [[FIRStorageMetadata alloc] initWithDictionary:metadata]; - if ([path hasPrefix:@"assets-library://"]) { - NSURL *localFile = [[NSURL alloc] initWithString:path]; + if ([localPath hasPrefix:@"assets-library://"]) { + NSURL *localFile = [[NSURL alloc] initWithString:localPath]; PHFetchResult* assets = [PHAsset fetchAssetsWithALAssetURLs:@[localFile] options:nil]; PHAsset *asset = [assets firstObject]; [[PHImageManager defaultManager] requestImageDataForAsset:asset - options:nil + options:nil resultHandler:^(NSData * imageData, NSString * dataUTI, UIImageOrientation orientation, NSDictionary * info) { - FIRStorageUploadTask *uploadTask = [uploadRef putData:imageData - metadata:firmetadata]; + FIRStorageUploadTask *uploadTask = [fileRef putData:imageData + metadata:firmetadata]; [self addUploadObservers:uploadTask + path:path callback:callback]; }]; } else { - NSURL *imageFile = [NSURL fileURLWithPath:path]; - FIRStorageUploadTask *uploadTask = [uploadRef putFile:imageFile - metadata:firmetadata]; + NSURL *imageFile = [NSURL fileURLWithPath:localPath]; + FIRStorageUploadTask *uploadTask = [fileRef putFile:imageFile + metadata:firmetadata]; [self addUploadObservers:uploadTask + path:path callback:callback]; } } - (void) addUploadObservers:(FIRStorageUploadTask *) uploadTask + path:(NSString *) path callback:(RCTResponseSenderBlock) callback { // Listen for state changes, errors, and completion of the upload. [uploadTask observeStatus:FIRStorageTaskStatusResume handler:^(FIRStorageTaskSnapshot *snapshot) { // Upload resumed, also fires when the upload starts - [self sendJSEvent:STORAGE_UPLOAD_RESUMED props:@{ - @"eventName": STORAGE_UPLOAD_RESUMED, - @"ref": snapshot.reference.bucket - }]; + NSDictionary *event = [self getUploadTaskAsDictionary:snapshot]; + [self sendJSEvent:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event]; }]; [uploadTask observeStatus:FIRStorageTaskStatusPause handler:^(FIRStorageTaskSnapshot *snapshot) { // Upload paused - [self sendJSEvent:STORAGE_UPLOAD_PAUSED props:@{ - @"eventName": STORAGE_UPLOAD_PAUSED, - @"ref": snapshot.reference.bucket - }]; + NSDictionary *event = [self getUploadTaskAsDictionary:snapshot]; + [self sendJSEvent:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event]; }]; [uploadTask observeStatus:FIRStorageTaskStatusProgress handler:^(FIRStorageTaskSnapshot *snapshot) { // Upload reported progress - float percentComplete; - if (snapshot.progress.totalUnitCount == 0) { - percentComplete = 0.0; - } else { - percentComplete = 100.0 * (snapshot.progress.completedUnitCount) / (snapshot.progress.totalUnitCount); - } - - [self sendJSEvent:STORAGE_UPLOAD_PROGRESS props:@{ - @"eventName": STORAGE_UPLOAD_PROGRESS, - @"progress": @(percentComplete) - }]; - + NSDictionary *event = [self getUploadTaskAsDictionary:snapshot]; + [self sendJSEvent:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event]; }]; [uploadTask observeStatus:FIRStorageTaskStatusSuccess handler:^(FIRStorageTaskSnapshot *snapshot) { - [uploadTask removeAllObservers]; - // Upload completed successfully - FIRStorageReference *ref = snapshot.reference; - NSDictionary *props = @{ - @"fullPath": ref.fullPath, - @"bucket": ref.bucket, - @"name": ref.name, - @"metadata": [snapshot.metadata dictionaryRepresentation] - }; - - callback(@[[NSNull null], props]); + NSDictionary *resp = [self getUploadTaskAsDictionary:snapshot]; + + [self sendJSEvent:STORAGE_EVENT path:path title:STORAGE_UPLOAD_SUCCESS props:resp]; + callback(@[[NSNull null], resp]); }]; [uploadTask observeStatus:FIRStorageTaskStatusFailure handler:^(FIRStorageTaskSnapshot *snapshot) { @@ -133,114 +237,89 @@ - (void) addUploadObservers:(FIRStorageUploadTask *) uploadTask switch (snapshot.error.code) { case FIRStorageErrorCodeObjectNotFound: // File doesn't exist - [errProps setValue:@"File does not exist" forKey:@"description"]; + [errProps setValue:@"File does not exist" forKey:@"message"]; break; case FIRStorageErrorCodeUnauthorized: // User doesn't have permission to access file - [errProps setValue:@"You do not have permissions" forKey:@"description"]; + [errProps setValue:@"You do not have permissions" forKey:@"message"]; break; case FIRStorageErrorCodeCancelled: // User canceled the upload - [errProps setValue:@"Upload cancelled" forKey:@"description"]; + [errProps setValue:@"Upload cancelled" forKey:@"message"]; break; case FIRStorageErrorCodeUnknown: // Unknown error occurred, inspect the server response - [errProps setValue:@"Unknown error" forKey:@"description"]; + [errProps setValue:@"Unknown error" forKey:@"message"]; break; } + //TODO: Error event callback(@[errProps]); }}]; } -RCT_EXPORT_METHOD(downloadFile: (NSString *) remotePath - localFile:(NSString *) file - callback:(RCTResponseSenderBlock) callback) +//Firebase.Storage methods +RCT_EXPORT_METHOD(setMaxDownloadRetryTime:(NSNumber *) milliseconds) { - FIRStorageReference *fileRef = [[FIRStorage storage] referenceWithPath:remotePath]; - NSURL *localFile = [NSURL fileURLWithPath:file]; - - FIRStorageDownloadTask *downloadTask = [fileRef writeToFile:localFile]; - // Listen for state changes, errors, and completion of the download. - [downloadTask observeStatus:FIRStorageTaskStatusResume handler:^(FIRStorageTaskSnapshot *snapshot) { - // Upload resumed, also fires when the upload starts - [self sendJSEvent:STORAGE_DOWNLOAD_RESUMED props:@{ - @"eventName": STORAGE_DOWNLOAD_RESUMED, - @"ref": snapshot.reference.bucket - }]; - }]; - - [downloadTask observeStatus:FIRStorageTaskStatusPause handler:^(FIRStorageTaskSnapshot *snapshot) { - // Upload paused - [self sendJSEvent:STORAGE_DOWNLOAD_PAUSED props:@{ - @"eventName": STORAGE_DOWNLOAD_PAUSED, - @"ref": snapshot.reference.bucket - }]; - }]; - [downloadTask observeStatus:FIRStorageTaskStatusProgress handler:^(FIRStorageTaskSnapshot *snapshot) { - // Upload reported progress - float percentComplete; - if (snapshot.progress.totalUnitCount == 0) { - percentComplete = 0.0; - } else { - percentComplete = 100.0 * (snapshot.progress.completedUnitCount) / (snapshot.progress.totalUnitCount); - } - - [self sendJSEvent:STORAGE_DOWNLOAD_PROGRESS props:@{ - @"eventName": STORAGE_DOWNLOAD_PROGRESS, - @"progress": @(percentComplete) - }]; - - }]; + [[FIRStorage storage] setMaxDownloadRetryTime:[milliseconds doubleValue]]; +} - [downloadTask observeStatus:FIRStorageTaskStatusSuccess handler:^(FIRStorageTaskSnapshot *snapshot) { - [downloadTask removeAllObservers]; +RCT_EXPORT_METHOD(setMaxOperationRetryTime:(NSNumber *) milliseconds) +{ + [[FIRStorage storage] setMaxOperationRetryTime:[milliseconds doubleValue]]; +} - // Upload completed successfully - FIRStorageReference *ref = snapshot.reference; - NSDictionary *props = @{ - @"fullPath": ref.fullPath, - @"bucket": ref.bucket, - @"name": ref.name - }; - - callback(@[[NSNull null], props]); - }]; +RCT_EXPORT_METHOD(setMaxUploadRetryTime:(NSNumber *) milliseconds) +{ + [[FIRStorage storage] setMaxUploadRetryTime:[milliseconds doubleValue]]; +} - [downloadTask observeStatus:FIRStorageTaskStatusFailure handler:^(FIRStorageTaskSnapshot *snapshot) { - if (snapshot.error != nil) { - NSDictionary *errProps = [[NSMutableDictionary alloc] init]; - NSLog(@"Error in download: %@", snapshot.error); +- (FIRStorageReference *)getReference:(NSString *)path +{ + if ([path hasPrefix:@"url::"]) { + NSString *url = [path substringFromIndex:5]; + return [[FIRStorage storage] referenceForURL:url]; + } else { + return [[FIRStorage storage] referenceWithPath:path]; + } +} - switch (snapshot.error.code) { - case FIRStorageErrorCodeObjectNotFound: - // File doesn't exist - [errProps setValue:@"File does not exist" forKey:@"description"]; - break; - case FIRStorageErrorCodeUnauthorized: - // User doesn't have permission to access file - [errProps setValue:@"You do not have permissions" forKey:@"description"]; - break; - case FIRStorageErrorCodeCancelled: - // User canceled the upload - [errProps setValue:@"Download canceled" forKey:@"description"]; - break; - case FIRStorageErrorCodeUnknown: - // Unknown error occurred, inspect the server response - [errProps setValue:@"Unknown error" forKey:@"description"]; - break; - } +- (NSDictionary *)getDownloadTaskAsDictionary:(FIRStorageTaskSnapshot *)task { + return @{ + @"bytesTransferred": @(task.progress.completedUnitCount), + @"ref": task.reference.fullPath, + @"status": [self getTaskStatus:task.status], + @"totalBytes": @(task.progress.totalUnitCount) + }; +} - callback(@[errProps]); - }}]; +- (NSDictionary *)getUploadTaskAsDictionary:(FIRStorageTaskSnapshot *)task +{ + NSString *downloadUrl = [task.metadata.downloadURL absoluteString]; + FIRStorageMetadata *metadata = [task.metadata dictionaryRepresentation]; + return @{ + @"bytesTransferred": @(task.progress.completedUnitCount), + @"downloadUrl": downloadUrl != nil ? downloadUrl : [NSNull null], + @"metadata": metadata != nil ? metadata : [NSNull null], + @"ref": task.reference.fullPath, + @"state": [self getTaskStatus:task.status], + @"totalBytes": @(task.progress.totalUnitCount) + }; } -// Compatibility with the android library -// For now, just passes the url path back -RCT_EXPORT_METHOD(getRealPathFromURI: (NSString *) urlStr - callback:(RCTResponseSenderBlock) callback) +- (NSString *)getTaskStatus:(FIRStorageTaskStatus)status { - callback(@[[NSNull null], urlStr]); + if (status == FIRStorageTaskStatusResume || status == FIRStorageTaskStatusProgress) { + return @"RUNNING"; + } else if (status == FIRStorageTaskStatusPause) { + return @"PAUSED"; + } else if (status == FIRStorageTaskStatusSuccess) { + return @"SUCCESS"; + } else if (status == FIRStorageTaskStatusFailure) { + return @"ERROR"; + } else { + return @"UNKNOWN"; + } } // This is just too good not to use, but I don't want to take credit for @@ -269,25 +348,36 @@ - (NSDictionary *)constantsToExport // Not sure how to get away from this... yet - (NSArray *)supportedEvents { - return @[ - STORAGE_UPLOAD_PAUSED, - STORAGE_UPLOAD_RESUMED, - STORAGE_UPLOAD_PROGRESS, - STORAGE_DOWNLOAD_PAUSED, - STORAGE_DOWNLOAD_RESUMED, - STORAGE_DOWNLOAD_PROGRESS - ]; + return @[STORAGE_EVENT, STORAGE_ERROR]; } -- (void) sendJSEvent:(NSString *)title +- (void) sendJSError:(NSError *) error + withPath:(NSString *) path +{ + NSDictionary *evt = @{ + @"path": path, + @"message": [error debugDescription] + }; + [self sendJSEvent:STORAGE_ERROR path:path title:STORAGE_ERROR props: evt]; +} + +- (void) sendJSEvent:(NSString *)type + path:(NSString *)path + title:(NSString *)title props:(NSDictionary *)props { @try { - [self sendEventWithName:title - body:props]; + [self sendEventWithName:type + body:@{ + @"eventName": title, + @"path": path, + @"body": props + }]; + } @catch (NSException *err) { NSLog(@"An error occurred in sendJSEvent: %@", [err debugDescription]); + NSLog(@"Tried to send: %@ with %@", title, props); } } diff --git a/lib/firestack.js b/lib/firestack.js index 947077a..93a56b8 100644 --- a/lib/firestack.js +++ b/lib/firestack.js @@ -176,16 +176,6 @@ export default class Firestack extends Singleton { return promisify('serverValue', FirestackModule)(); } - // TODO what are these for? - get app(): Object { - return this.appInstance; - } - - // TODO what are these for? - getInstance(): Object { - return this.appInstance; - } - get apps(): Array { return Object.keys(instances); } diff --git a/lib/modules/analytics.js b/lib/modules/analytics.js index ac2b062..4cd1b90 100644 --- a/lib/modules/analytics.js +++ b/lib/modules/analytics.js @@ -28,7 +28,7 @@ export default class Analytics extends Base { * @param params * @return {Promise} */ - logEvent(name: string, params: Object = {}): void { + logEvent(name: string, params: Object): void { // check name is not a reserved event name if (ReservedEventNames.includes(name)) { throw new Error(`event name '${name}' is a reserved event name and can not be used.`); @@ -40,7 +40,7 @@ export default class Analytics extends Base { } // maximum number of allowed params check - if (Object.keys(params).length > 25) throw new Error('Maximum number of parameters exceeded (25).'); + if (params && Object.keys(params).length > 25) throw new Error('Maximum number of parameters exceeded (25).'); // TODO validate param names and values // Parameter names can be up to 24 characters long and must start with an alphabetic character @@ -101,6 +101,17 @@ export default class Analytics extends Base { return FirestackAnalytics.setUserProperty(name, value); } + /** + * Sets a user property to a given value. + * @param object + */ + setUserProperties(object: Object): void { + for (const property of Object.keys(object)) { + FirestackAnalytics.setUserProperty(property, object[property]); + } + } + + get namespace(): string { return 'firestack:analytics'; } diff --git a/lib/modules/base.js b/lib/modules/base.js index 1704fe4..ef25fc2 100644 --- a/lib/modules/base.js +++ b/lib/modules/base.js @@ -35,20 +35,6 @@ export class Base extends EventEmitter { return logs[this.namespace]; } - // TODO unused - do we need this anymore? - _addConstantExports(constants) { - Object.keys(constants).forEach((name) => { - FirestackModule[name] = constants[name]; - }); - } - - // TODO unused - do we need this anymore? - _addToFirestackInstance(...methods: Array) { - methods.forEach((name) => { - this.firestack[name] = this[name].bind(this); - }); - } - /** * app instance **/ @@ -91,26 +77,12 @@ export class Base extends EventEmitter { } export class ReferenceBase extends Base { - constructor(firestack: Object, path: Array | string) { + constructor(firestack: Object, path: string) { super(firestack); - - this.path = Array.isArray(path) ? path : (typeof path === 'string' ? [path] : []); - - // sanitize path, just in case - this.path = this.path.filter(str => str !== ''); + this.path = path || '/'; } get key(): string { - const path = this.path; - return path.length === 0 ? '/' : path[path.length - 1]; - } - - pathToString(): string { - const path = this.path; - let pathStr = (path.length > 0 ? path.join('/') : '/'); - if (pathStr[0] != '/') { - pathStr = `/${pathStr}`; - } - return pathStr; + return this.path === '/' ? null : this.path.substring(this.path.lastIndexOf('/') + 1); } } diff --git a/lib/modules/database/disconnect.js b/lib/modules/database/disconnect.js index 275380e..bce7f83 100644 --- a/lib/modules/database/disconnect.js +++ b/lib/modules/database/disconnect.js @@ -17,7 +17,7 @@ export default class Disconnect { } setValue(val: string | Object) { - const path = this.ref.dbPath(); + const path = this.ref._dbPath(); if (typeof val === 'string') { return promisify('onDisconnectSetString', FirestackDatabase)(path, val); } else if (typeof val === 'object') { @@ -26,10 +26,10 @@ export default class Disconnect { } remove() { - return promisify('onDisconnectRemove', FirestackDatabase)(this.ref.dbPath()); + return promisify('onDisconnectRemove', FirestackDatabase)(this.ref._dbPath()); } cancel() { - return promisify('onDisconnectCancel', FirestackDatabase)(this.ref.dbPath()); + return promisify('onDisconnectCancel', FirestackDatabase)(this.ref._dbPath()); } } diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index f4477e7..fe0a21a 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -3,12 +3,12 @@ * Database representation wrapper */ import { NativeModules, NativeEventEmitter } from 'react-native'; + import { Base } from './../base'; import Snapshot from './snapshot.js'; import Reference from './reference.js'; import { promisify } from './../../utils'; -const FirestackModule = NativeModules.Firestack; const FirestackDatabase = NativeModules.FirestackDatabase; const FirestackDatabaseEvt = new NativeEventEmitter(FirestackDatabase); @@ -34,6 +34,7 @@ export default class Database extends Base { ); this.offsetRef = this.ref('.info/serverTimeOffset'); + this.offsetRef.on('value', (snapshot) => { this.serverTimeOffset = snapshot.val() || this.serverTimeOffset; }); @@ -41,9 +42,14 @@ export default class Database extends Base { this.log.debug('Created new Database instance', this.options); } - get ServerValue() { + /** + * https://firebase.google.com/docs/reference/js/firebase.database.ServerValue + * @returns {{TIMESTAMP: (*|{[.sv]: string})}} + * @constructor + */ + get ServerValue(): Object { return { - TIMESTAMP: FirestackModule.serverValueTimestamp || { '.sv': 'timestamp' }, + TIMESTAMP: FirestackDatabase.serverValueTimestamp || { '.sv': 'timestamp' }, }; } @@ -52,7 +58,7 @@ export default class Database extends Base { * @param path * @returns {Reference} */ - ref(...path: Array) { + ref(path: string) { return new Reference(this, path); } diff --git a/lib/modules/database/query.js b/lib/modules/database/query.js index baa6942..90f451f 100644 --- a/lib/modules/database/query.js +++ b/lib/modules/database/query.js @@ -6,9 +6,6 @@ import { ReferenceBase } from './../base'; import Reference from './reference.js'; -// TODO why randomly 1000000? comments? -let uid = 1000000; - /** * @class Query */ @@ -19,10 +16,9 @@ export default class Query extends ReferenceBase { ref: Reference; - constructor(ref: Reference, path: Array, existingModifiers?: Array) { + constructor(ref: Reference, path: string, existingModifiers?: Array) { super(ref.db, path); this.log.debug('creating Query ', path, existingModifiers); - this.uid = uid++; // uuid.v4(); this.ref = ref; this.modifiers = existingModifiers ? [...existingModifiers] : []; } diff --git a/lib/modules/database/reference.js b/lib/modules/database/reference.js index 3471383..e2ded4b 100644 --- a/lib/modules/database/reference.js +++ b/lib/modules/database/reference.js @@ -21,7 +21,7 @@ export default class Reference extends ReferenceBase { db: FirestackDatabase; query: Query; - constructor(db: FirestackDatabase, path: Array, existingModifiers?: Array) { + constructor(db: FirestackDatabase, path: string, existingModifiers?: Array) { super(db.firestack, path); this.db = db; this.namespace = 'firestack:db:ref'; @@ -77,16 +77,15 @@ export default class Reference extends ReferenceBase { */ push(value: any, onComplete: Function) { if (value === null || value === undefined) { - // todo add server timestamp to push id call. - const _paths = this.path.concat([generatePushID(this.db.serverTimeOffset)]); - return new Reference(this.db, _paths); + const _path = this.path + '/' + generatePushID(this.db.serverTimeOffset); + return new Reference(this.db, _path); } const path = this._dbPath(); const _value = this._serializeAnyType(value); return promisify('push', FirestackDatabase)(path, _value) .then(({ ref }) => { - const newRef = new Reference(this.db, ref.split('/')); + const newRef = new Reference(this.db, ref); if (isFunction(onComplete)) return onComplete(null, newRef); return newRef; }).catch((e) => { @@ -253,19 +252,12 @@ export default class Reference extends ReferenceBase { return newRef; } - // TODO why is this presence here on DB ref? its unrelated? - presence(path: string) { - const presence = this.firestack.presence; - const ref = path ? this.child(path) : this; - return presence.ref(ref, this._dbPath()); - } - onDisconnect() { return new Disconnect(this); } child(path: string) { - return new Reference(this.db, this.path.concat(path.split('/'))); + return new Reference(this.db, this.path + '/' + path); } toString(): string { @@ -276,22 +268,13 @@ export default class Reference extends ReferenceBase { * GETTERS */ - /** - * Returns the current key of this ref - i.e. /foo/bar returns 'bar' - * @returns {*} - */ - get key(): string|null { - if (!this.path.length) return null; - return this.path.slice(this.path.length - 1, this.path.length)[0]; - } - /** * Returns the parent ref of the current ref i.e. a ref of /foo/bar would return a new ref to '/foo' * @returns {*} */ get parent(): Reference|null { - if (!this.path.length || this.path.length === 1) return null; - return new Reference(this.db, this.path.slice(0, -1)); + if (this.path === '/') return null; + return new Reference(this.db, this.path.substring(0, this.path.lastIndexOf('/'))); } @@ -300,22 +283,15 @@ export default class Reference extends ReferenceBase { * @returns {Reference} */ get root(): Reference { - return new Reference(this.db, []); + return new Reference(this.db, '/'); } /** * INTERNALS */ - _dbPath(paths?: Array): string { - const path = paths || this.path; - const pathStr = (path.length > 0 ? path.join('/') : '/'); - - if (pathStr[0] !== '/') { - return `/${pathStr}`; - } - - return pathStr; + _dbPath(): string { + return this.path; } /** diff --git a/lib/modules/storage/index.js b/lib/modules/storage/index.js index 95718f2..f3f2417 100644 --- a/lib/modules/storage/index.js +++ b/lib/modules/storage/index.js @@ -15,55 +15,82 @@ type StorageOptionsType = { export default class Storage extends Base { constructor(firestack: Object, options: StorageOptionsType = {}) { super(firestack, options); - this.refs = {}; + this.subscriptions = {}; + + this.successListener = FirestackStorageEvt.addListener( + 'storage_event', + event => this._handleStorageEvent(event) + ); + + this.errorListener = FirestackStorageEvt.addListener( + 'storage_error', + err => this._handleStorageError(err) + ); } - ref(...path: Array): StorageRef { - const key = this._pathKey(...path); - if (!this.refs[key]) { - this.refs[key] = new StorageRef(this, path); - } - return this.refs[key]; + ref(path: string): StorageRef { + return new StorageRef(this, path); + } + + refFromURL(url: string): Promise { + return new StorageRef(this, `url::${url}`); + } + + setMaxOperationRetryTime(time: number) { + FirestackStorage.setMaxOperationRetryTime(time); + } + + setMaxUploadRetryTime(time: number) { + FirestackStorage.setMaxUploadRetryTime(time); } - /** - * Upload a file path - * @param {string} name The destination for the file - * @param {string} filePath The local path of the file - * @param {object} metadata An object containing metadata - * @param listener - * @return {Promise} - */ - uploadFile(name: string, filePath: string, metadata: Object = {}, listener: Function = noop): Promise { - const _filePath = filePath.replace('file://', ''); - this.log.debug('uploadFile(', _filePath, ') -> ', name); - const listeners = [ - this._addListener('upload_paused', listener), - this._addListener('upload_resumed', listener), - this._addListener('upload_progress', listener), - ]; - - return promisify('uploadFile', FirestackStorage)(name, _filePath, metadata) - .then((res) => { - listeners.forEach(l => l.remove()); - return res; + //Additional methods compared to Web API + setMaxDownloadRetryTime(time: number) { + FirestackStorage.setMaxDownloadRetryTime(time); + } + + _handleStorageEvent(event: Object) { + const { path, eventName } = event; + const body = event.body || {}; + + this.log.debug('_handleStorageEvent: ', path, eventName, body); + + if (this.subscriptions[path] && this.subscriptions[path][eventName]) { + this.subscriptions[path][eventName].forEach((cb) => { + cb(body); }) - .catch((err) => { - this.log.error('Error uploading file ', name, ' to ', _filePath, '. Error: ', err); - throw err; - }); + } } - getRealPathFromURI(uri: string): Promise { - return promisify('getRealPathFromURI', FirestackStorage)(uri); + _handleStorageError(err: Object) { + this.log.debug('_handleStorageError ->', err); } - _addListener(evt: string, cb: (evt: Object) => Object): {remove: () => void} { - return FirestackStorageEvt.addListener(evt, cb); + _addListener(path: string, eventName: string, cb: (evt: Object) => Object) { + if (!this.subscriptions[path]) this.subscriptions[path] = {}; + if (!this.subscriptions[path][eventName]) this.subscriptions[path][eventName] = []; + this.subscriptions[path][eventName].push(cb); } - _pathKey(...path: Array): string { - return path.join('-'); + _removeListener(path: string, eventName: string, origCB: (evt: Object) => Object) { + if (!this.subscriptions[path] || (eventName && !this.subscriptions[path][eventName])) { + this.log.warn('_removeListener() called, but not currently listening at that location (bad path)', path, eventName); + return Promise.resolve(); + } + + if (eventName && origCB) { + const i = this.subscriptions[path][eventName].indexOf(origCB); + + if (i === -1) { + this.log.warn('_removeListener() called, but the callback specified is not listening at this location (bad path)', path, eventName); + } else { + this.subscriptions[path][eventName].splice(i, 1); + } + } else if (eventName) { + this.subscriptions[path][eventName] = []; + } else { + this.subscriptions[path] = {} + } } static constants = { diff --git a/lib/modules/storage/reference.js b/lib/modules/storage/reference.js index e70a93d..267153f 100644 --- a/lib/modules/storage/reference.js +++ b/lib/modules/storage/reference.js @@ -8,46 +8,138 @@ import Storage from './'; const FirestackStorage = NativeModules.FirestackStorage; export default class StorageRef extends ReferenceBase { - constructor(storage: Storage, path: Array) { + constructor(storage: Storage, path: string) { super(storage.firestack, path); - this.storage = storage; } - downloadUrl(): Promise { - const path = this.pathToString(); - this.log.debug('downloadUrl(', path, ')'); - return promisify('downloadUrl', FirestackStorage)(path) + child(path: string) { + return new StorageRef(this.storage, this.path + '/' + path); + } + + delete(): Promise { + return promisify('delete', FirestackStorage)(this.path) + .catch(err => { + this.log.error('Error deleting reference ', this.path, '. Error: ', err); + throw err; + }) + } + + getDownloadURL(): Promise { + this.log.debug('getDownloadURL(', this.path, ')'); + return promisify('getDownloadURL', FirestackStorage)(this.path) .catch((err) => { - this.log.error('Error downloading URL for ', path, '. Error: ', err); + this.log.error('Error downloading URL for ', this.path, '. Error: ', err); throw err; }); } + getMetadata(): Promise { + return promisify('getMetadata', FirestackStorage)(this.path) + .catch(err => { + this.log.error('Error getting metadata for ', this.path, '. Error: ', err); + throw err; + }) + } + + //TODO: Figure out the best way to do this on iOS/Android + put(data: Object, metadata: Object = {}): /*UploadTask*/Promise { + throw new Error('put() is not currently supported by react-native-firestack') + } + + //TODO: Figure out the best way to do this on iOS/Android + putString(data: string, format: String, metadata: Object = {}): /*UploadTask*/Promise { + throw new Error('putString() is not currently supported by react-native-firestack') + } + + toString(): String { + //TODO: Return full gs://bucket/path + return this.path; + } + + updateMetadata(metadata: Object = {}): Promise { + return promisify('updateMetadata', FirestackStorage)(this.path, metadata) + .catch(err => { + this.log.error('Error updating metadata for ', this.path, '. Error: ', err); + throw err; + }) + } + + //Additional methods compared to Web API + + //TODO: Listeners /** * Downloads a reference to the device - * @param {String} downloadPath Where to store the file - * @param listener + * @param {String} filePath Where to store the file * @return {Promise} */ - download(downloadPath: string, listener: Function = noop): Promise { - const path = this.pathToString(); - this.log.debug('download(', path, ') -> ', downloadPath); - const listeners = [ - this.storage._addListener('download_progress', listener), - this.storage._addListener('download_paused', listener), - this.storage._addListener('download_resumed', listener), - ]; - - return promisify('downloadFile', FirestackStorage)(path, downloadPath) - .then((res) => { - this.log.debug('res --->', res); - listeners.forEach(l => l.remove()); - return res; - }) - .catch((err) => { - this.log.error('Error downloading ', path, ' to ', downloadPath, '. Error: ', err); - throw err; - }); + downloadFile(filePath: string): Promise { + this.log.debug('download(', this.path, ') -> ', filePath); + + let downloadTask = promisify('downloadFile', FirestackStorage)(this.path, filePath); + downloadTask.cancel = () => { + //TODO + throw new Error('.cancel() is not currently supported by react-native-firestack'); + } + downloadTask.on = (event, nextOrObserver, error, complete) => { + //TODO: nextOrObserver as an object + if (nextOrObserver) this.storage._addListener(this.path, 'state_changed', nextOrObserver); + if (error) this.storage._addListener(this.path, 'download_failure', error); + if (complete) this.storage._addListener(this.path, 'download_success', complete); + return () => { + if (nextOrObserver) this.storage._removeListener(this.path, 'state_changed', nextOrObserver); + if (error) this.storage._removeListener(this.path, 'download_failure', error); + if (complete) this.storage._removeListener(this.path, 'download_success', complete); + } + } + downloadTask.pause = () => { + //TODO + throw new Error('.pause() is not currently supported by react-native-firestack'); + } + downloadTask.resume = () => { + //TODO + throw new Error('.resume() is not currently supported by react-native-firestack'); + } + + return downloadTask; + } + + /** + * Upload a file path + * @param {string} filePath The local path of the file + * @param {object} metadata An object containing metadata + * @return {Promise} + */ + putFile(filePath: Object, metadata: Object = {}): Promise { + const _filePath = filePath.replace('file://', ''); + this.log.debug('putFile(', _filePath, ') -> ', this.path); + + //TODO: There's probably a better way of doing this, but I couldn't figure out the best way to extend a promise + let uploadTask = promisify('putFile', FirestackStorage)(this.path, _filePath, metadata); + uploadTask.cancel = () => { + //TODO + throw new Error('.cancel() is not currently supported by react-native-firestack'); + } + uploadTask.on = (event, nextOrObserver, error, complete) => { + //TODO: nextOrObserver as an object + if (nextOrObserver) this.storage._addListener(this.path, 'state_changed', nextOrObserver); + if (error) this.storage._addListener(this.path, 'upload_failure', error); + if (complete) this.storage._addListener(this.path, 'upload_success', complete); + return () => { + if (nextOrObserver) this.storage._removeListener(this.path, 'state_changed', nextOrObserver); + if (error) this.storage._removeListener(this.path, 'upload_failure', error); + if (complete) this.storage._removeListener(this.path, 'upload_success', complete); + } + } + uploadTask.pause = () => { + //TODO + throw new Error('.pause() is not currently supported by react-native-firestack'); + } + uploadTask.resume = () => { + //TODO + throw new Error('.resume() is not currently supported by react-native-firestack'); + } + + return uploadTask; } }