Release React Native for Android

This is an early release and there are several things that are known
not to work if you're porting your iOS app to Android.

See the Known Issues guide on the website.

We will work with the community to reach platform parity with iOS.
This commit is contained in:
Martin Konicek
2015-09-14 15:35:58 +01:00
parent c372dab213
commit 42eb5464fd
571 changed files with 44550 additions and 116 deletions

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.common;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.CatalystInstance;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.common.ReactConstants;
/**
* Cleans sensitive user data from native modules that implement the {@code Cleanable} interface.
* This is useful e.g. when a user logs out from an app.
*/
public class ModuleDataCleaner {
/**
* Indicates a module may contain sensitive user data and should be cleaned on logout.
*
* Types of data that should be cleaned:
* - Persistent data (disk) that may contain user information or content.
* - Retained (static) in-memory data that may contain user info or content.
*
* Note that the following types of modules do not need to be cleaned here:
* - Modules whose user data is kept in memory in non-static fields, assuming the app uses a
* separate instance for each viewer context.
* - Modules that remove all persistent data (temp files, etc) when the catalyst instance is
* destroyed. This is because logout implies that the instance is destroyed. Apps should enforce
* this.
*/
public interface Cleanable {
void clearSensitiveData();
}
public static void cleanDataFromModules(CatalystInstance catalystInstance) {
for (NativeModule nativeModule : catalystInstance.getNativeModules()) {
if (nativeModule instanceof Cleanable) {
FLog.d(ReactConstants.TAG, "Cleaning data from " + nativeModule.getName());
((Cleanable) nativeModule).clearSensitiveData();
}
}
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.core;
/**
* Interface used by {@link DeviceEventManagerModule} to delegate hardware back button events. It's
* suppose to provide a default behavior since it would be triggered in the case when JS side
* doesn't want to handle back press events.
*/
public interface DefaultHardwareBackBtnHandler {
/**
* By default, all onBackPress() calls should not execute the default backpress handler and should
* instead propagate it to the JS instance. If JS doesn't want to handle the back press itself,
* it shall call back into native to invoke this function which should execute the default handler
*/
void invokeDefaultOnBackPressed();
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.core;
import javax.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.UiThreadUtil;
/**
* Native module that handles device hardware events like hardware back presses.
*/
public class DeviceEventManagerModule extends ReactContextBaseJavaModule {
public static interface RCTDeviceEventEmitter extends JavaScriptModule {
void emit(String eventName, @Nullable Object data);
}
private final Runnable mInvokeDefaultBackPressRunnable;
public DeviceEventManagerModule(
ReactApplicationContext reactContext,
final DefaultHardwareBackBtnHandler backBtnHandler) {
super(reactContext);
mInvokeDefaultBackPressRunnable = new Runnable() {
@Override
public void run() {
UiThreadUtil.assertOnUiThread();
backBtnHandler.invokeDefaultOnBackPressed();
}
};
}
/**
* Sends an event to the JS instance that the hardware back has been pressed.
*/
public void emitHardwareBackPressed() {
getReactApplicationContext()
.getJSModule(RCTDeviceEventEmitter.class)
.emit("hardwareBackPress", null);
}
/**
* Invokes the default back handler for the host of this catalyst instance. This should be invoked
* if JS does not want to handle the back press itself.
*/
@ReactMethod
public void invokeDefaultBackPressHandler() {
getReactApplicationContext().runOnUiQueueThread(mInvokeDefaultBackPressRunnable);
}
@Override
public String getName() {
return "DeviceEventManager";
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.core;
import java.io.File;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.devsupport.DevSupportManager;
import com.facebook.react.common.ReactConstants;
public class ExceptionsManagerModule extends BaseJavaModule {
private final DevSupportManager mDevSupportManager;
public ExceptionsManagerModule(DevSupportManager devSupportManager) {
mDevSupportManager = devSupportManager;
}
@Override
public String getName() {
return "RKExceptionsManager";
}
private String stackTraceToString(ReadableArray stack) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < stack.size(); i++) {
ReadableMap frame = stack.getMap(i);
stringBuilder.append(frame.getString("methodName"));
stringBuilder.append("\n ");
stringBuilder.append(new File(frame.getString("file")).getName());
stringBuilder.append(":");
stringBuilder.append(frame.getInt("lineNumber"));
if (frame.hasKey("column") && !frame.isNull("column")) {
stringBuilder
.append(":")
.append(frame.getInt("column"));
}
stringBuilder.append("\n");
}
return stringBuilder.toString();
}
@ReactMethod
public void reportFatalException(String title, ReadableArray details, int exceptionId) {
showOrThrowError(title, details, exceptionId);
}
@ReactMethod
public void reportSoftException(String title, ReadableArray details) {
FLog.e(ReactConstants.TAG, title + "\n" + stackTraceToString(details));
}
private void showOrThrowError(String title, ReadableArray details, int exceptionId) {
if (mDevSupportManager.getDevSupportEnabled()) {
mDevSupportManager.showNewJSError(title, details, exceptionId);
} else {
throw new JavascriptException(stackTraceToString(details));
}
}
@ReactMethod
public void updateExceptionMessage(String title, ReadableArray details, int exceptionId) {
if (mDevSupportManager.getDevSupportEnabled()) {
mDevSupportManager.updateJSError(title, details, exceptionId);
}
}
}

View File

@@ -0,0 +1,18 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.core;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.WritableArray;
public interface JSTimersExecution extends JavaScriptModule {
public void callTimers(WritableArray timerIDs);
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.core;
/**
* A JS exception that was propagated to native. In debug mode, these exceptions are normally shown
* to developers in a redbox.
*/
public class JavascriptException extends RuntimeException {
public JavascriptException(String jsStackTrace) {
super(jsStackTrace);
}
}

View File

@@ -0,0 +1,204 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.core;
import javax.annotation.Nullable;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import android.util.SparseArray;
import android.view.Choreographer;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.uimanager.ReactChoreographer;
import com.facebook.react.common.SystemClock;
import com.facebook.infer.annotation.Assertions;
/**
* Native module for JS timer execution. Timers fire on frame boundaries.
*/
public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener {
private static class Timer {
private final int mCallbackID;
private final boolean mRepeat;
private final int mInterval;
private long mTargetTime;
private Timer(int callbackID, long initialTargetTime, int duration, boolean repeat) {
mCallbackID = callbackID;
mTargetTime = initialTargetTime;
mInterval = duration;
mRepeat = repeat;
}
}
private class FrameCallback implements Choreographer.FrameCallback {
/**
* Calls all timers that have expired since the last time this frame callback was called.
*/
@Override
public void doFrame(long frameTimeNanos) {
if (isPaused.get()) {
return;
}
long frameTimeMillis = frameTimeNanos / 1000000;
WritableArray timersToCall = null;
synchronized (mTimerGuard) {
while (!mTimers.isEmpty() && mTimers.peek().mTargetTime < frameTimeMillis) {
Timer timer = mTimers.poll();
if (timersToCall == null) {
timersToCall = Arguments.createArray();
}
timersToCall.pushInt(timer.mCallbackID);
if (timer.mRepeat) {
timer.mTargetTime = frameTimeMillis + timer.mInterval;
mTimers.add(timer);
} else {
mTimerIdsToTimers.remove(timer.mCallbackID);
}
}
}
if (timersToCall != null) {
Assertions.assertNotNull(mJSTimersModule).callTimers(timersToCall);
}
mReactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this);
}
}
private final Object mTimerGuard = new Object();
private final PriorityQueue<Timer> mTimers;
private final SparseArray<Timer> mTimerIdsToTimers;
private final AtomicBoolean isPaused = new AtomicBoolean(false);
private final ReactChoreographer mReactChoreographer;
private final FrameCallback mFrameCallback = new FrameCallback();
private @Nullable JSTimersExecution mJSTimersModule;
private boolean mFrameCallbackPosted = false;
public Timing(ReactApplicationContext reactContext) {
super(reactContext);
mReactChoreographer = ReactChoreographer.getInstance();
// We store timers sorted by finish time.
mTimers = new PriorityQueue<Timer>(
11, // Default capacity: for some reason they don't expose a (Comparator) constructor
new Comparator<Timer>() {
@Override
public int compare(Timer lhs, Timer rhs) {
long diff = lhs.mTargetTime - rhs.mTargetTime;
if (diff == 0) {
return 0;
} else if (diff < 0) {
return -1;
} else {
return 1;
}
}
});
mTimerIdsToTimers = new SparseArray<Timer>();
}
@Override
public void initialize() {
mJSTimersModule = getReactApplicationContext().getCatalystInstance()
.getJSModule(JSTimersExecution.class);
getReactApplicationContext().addLifecycleEventListener(this);
setChoreographerCallback();
}
@Override
public void onHostPause() {
isPaused.set(true);
clearChoreographerCallback();
}
@Override
public void onHostDestroy() {
clearChoreographerCallback();
}
@Override
public void onHostResume() {
isPaused.set(false);
// TODO(5195192) Investigate possible problems related to restarting all tasks at the same
// moment
setChoreographerCallback();
}
@Override
public void onCatalystInstanceDestroy() {
clearChoreographerCallback();
}
private void setChoreographerCallback() {
if (!mFrameCallbackPosted) {
mReactChoreographer.postFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS,
mFrameCallback);
mFrameCallbackPosted = true;
}
}
private void clearChoreographerCallback() {
if (mFrameCallbackPosted) {
mReactChoreographer.removeFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS,
mFrameCallback);
mFrameCallbackPosted = false;
}
}
@Override
public String getName() {
return "RKTiming";
}
@ReactMethod
public void createTimer(
final int callbackID,
final int duration,
final double jsSchedulingTime,
final boolean repeat) {
// Adjust for the amount of time it took for native to receive the timer registration call
long adjustedDuration = (long) Math.max(
0,
jsSchedulingTime - SystemClock.currentTimeMillis() + duration);
long initialTargetTime = SystemClock.nanoTime() / 1000000 + adjustedDuration;
Timer timer = new Timer(callbackID, initialTargetTime, duration, repeat);
synchronized (mTimerGuard) {
mTimers.add(timer);
mTimerIdsToTimers.put(callbackID, timer);
}
}
@ReactMethod
public void deleteTimer(int timerId) {
synchronized (mTimerGuard) {
Timer timer = mTimerIdsToTimers.get(timerId);
if (timer != null) {
// We may have already called/removed it
mTimerIdsToTimers.remove(timerId);
mTimers.remove(timer);
}
}
}
}

View File

@@ -0,0 +1,120 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.debug;
import javax.annotation.Nullable;
import java.util.Locale;
import android.os.Build;
import android.view.Choreographer;
import android.widget.Toast;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.JSApplicationCausedNativeException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.common.ReactConstants;
/**
* Module that records debug information during transitions (animated navigation events such as
* going from one screen to another).
*/
public class AnimationsDebugModule extends ReactContextBaseJavaModule {
private @Nullable FpsDebugFrameCallback mFrameCallback;
private final DeveloperSettings mCatalystSettings;
public AnimationsDebugModule(
ReactApplicationContext reactContext,
DeveloperSettings catalystSettings) {
super(reactContext);
mCatalystSettings = catalystSettings;
}
@Override
public String getName() {
return "AnimationsDebugModule";
}
@ReactMethod
public void startRecordingFps() {
if (!mCatalystSettings.isAnimationFpsDebugEnabled()) {
return;
}
if (mFrameCallback != null) {
throw new JSApplicationCausedNativeException("Already recording FPS!");
}
checkAPILevel();
mFrameCallback = new FpsDebugFrameCallback(
Choreographer.getInstance(),
getReactApplicationContext());
mFrameCallback.startAndRecordFpsAtEachFrame();
}
/**
* Called when an animation finishes. The caller should include the animation stop time in ms
* (unix time) so that we know when the animation stopped from the JS perspective and we don't
* count time after as being part of the animation.
*/
@ReactMethod
public void stopRecordingFps(double animationStopTimeMs) {
if (mFrameCallback == null) {
return;
}
checkAPILevel();
mFrameCallback.stop();
// Casting to long is safe here since animationStopTimeMs is unix time and thus relatively small
FpsDebugFrameCallback.FpsInfo fpsInfo = mFrameCallback.getFpsInfo((long) animationStopTimeMs);
if (fpsInfo == null) {
Toast.makeText(getReactApplicationContext(), "Unable to get FPS info", Toast.LENGTH_LONG);
} else {
String fpsString = String.format(
Locale.US,
"FPS: %.2f, %d frames (%d expected)",
fpsInfo.fps,
fpsInfo.totalFrames,
fpsInfo.totalExpectedFrames);
String jsFpsString = String.format(
Locale.US,
"JS FPS: %.2f, %d frames (%d expected)",
fpsInfo.jsFps,
fpsInfo.totalJsFrames,
fpsInfo.totalExpectedFrames);
String debugString = fpsString + "\n" + jsFpsString + "\n" +
"Total Time MS: " + String.format(Locale.US, "%d", fpsInfo.totalTimeMs);
FLog.d(ReactConstants.TAG, debugString);
Toast.makeText(getReactApplicationContext(), debugString, Toast.LENGTH_LONG).show();
}
mFrameCallback = null;
}
@Override
public void onCatalystInstanceDestroy() {
if (mFrameCallback != null) {
mFrameCallback.stop();
mFrameCallback = null;
}
}
private static void checkAPILevel() {
if (Build.VERSION.SDK_INT < 16) {
throw new JSApplicationCausedNativeException(
"Animation debugging is not supported in API <16");
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.debug;
/**
* Provides access to React Native developers settings.
*/
public interface DeveloperSettings {
/**
* @return whether an overlay showing current FPS should be shown.
*/
boolean isFpsDebugEnabled();
/**
* @return Whether debug information about transitions should be displayed.
*/
boolean isAnimationFpsDebugEnabled();
}

View File

@@ -0,0 +1,175 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.debug;
import android.view.Choreographer;
import com.facebook.react.bridge.ReactBridge;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
import com.facebook.react.common.LongArray;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.debug.NotThreadSafeUiManagerDebugListener;
/**
* Debug object that listens to bridge busy/idle events and UiManagerModule dispatches and uses it
* to calculate whether JS was able to update the UI during a given frame. After being installed
* on a {@link ReactBridge} and a {@link UIManagerModule},
* {@link #getDidJSHitFrameAndCleanup} should be called once per frame via a
* {@link Choreographer.FrameCallback}.
*/
public class DidJSUpdateUiDuringFrameDetector implements NotThreadSafeBridgeIdleDebugListener,
NotThreadSafeUiManagerDebugListener {
private final LongArray mTransitionToIdleEvents = LongArray.createWithInitialCapacity(20);
private final LongArray mTransitionToBusyEvents = LongArray.createWithInitialCapacity(20);
private final LongArray mViewHierarchyUpdateEnqueuedEvents =
LongArray.createWithInitialCapacity(20);
private final LongArray mViewHierarchyUpdateFinishedEvents =
LongArray.createWithInitialCapacity(20);
private volatile boolean mWasIdleAtEndOfLastFrame = true;
@Override
public synchronized void onTransitionToBridgeIdle() {
mTransitionToIdleEvents.add(System.nanoTime());
}
@Override
public synchronized void onTransitionToBridgeBusy() {
mTransitionToBusyEvents.add(System.nanoTime());
}
@Override
public synchronized void onViewHierarchyUpdateEnqueued() {
mViewHierarchyUpdateEnqueuedEvents.add(System.nanoTime());
}
@Override
public synchronized void onViewHierarchyUpdateFinished() {
mViewHierarchyUpdateFinishedEvents.add(System.nanoTime());
}
/**
* Designed to be called from a {@link Choreographer.FrameCallback#doFrame} call.
*
* There are two 'success' cases that will cause {@link #getDidJSHitFrameAndCleanup} to
* return true for a given frame:
*
* 1) UIManagerModule finished dispatching a batched UI update on the UI thread during the frame.
* This means that during the next hierarchy traversal, new UI will be drawn if needed (good).
* 2) The bridge ended the frame idle (meaning there were no JS nor native module calls still in
* flight) AND there was no UiManagerModule update enqueued that didn't also finish. NB: if
* there was one enqueued that actually finished, we'd have case 1), so effectively we just
* look for whether one was enqueued.
*
* NB: This call can only be called once for a given frame time range because it cleans up
* events it recorded for that frame.
*
* NB2: This makes the assumption that onViewHierarchyUpdateEnqueued is called from the
* {@link UIManagerModule#onBatchComplete()}, e.g. while the bridge is still considered busy,
* which means there is no race condition where the bridge has gone idle but a hierarchy update is
* waiting to be enqueued.
*
* @param frameStartTimeNanos the time in nanos that the last frame started
* @param frameEndTimeNanos the time in nanos that the last frame ended
*/
public synchronized boolean getDidJSHitFrameAndCleanup(
long frameStartTimeNanos,
long frameEndTimeNanos) {
// Case 1: We dispatched a UI update
boolean finishedUiUpdate = hasEventBetweenTimestamps(
mViewHierarchyUpdateFinishedEvents,
frameStartTimeNanos,
frameEndTimeNanos);
boolean didEndFrameIdle = didEndFrameIdle(frameStartTimeNanos, frameEndTimeNanos);
boolean hitFrame;
if (finishedUiUpdate) {
hitFrame = true;
} else {
// Case 2: Ended idle but no UI was enqueued during that frame
hitFrame = didEndFrameIdle && !hasEventBetweenTimestamps(
mViewHierarchyUpdateEnqueuedEvents,
frameStartTimeNanos,
frameEndTimeNanos);
}
cleanUp(mTransitionToIdleEvents, frameEndTimeNanos);
cleanUp(mTransitionToBusyEvents, frameEndTimeNanos);
cleanUp(mViewHierarchyUpdateEnqueuedEvents, frameEndTimeNanos);
cleanUp(mViewHierarchyUpdateFinishedEvents, frameEndTimeNanos);
mWasIdleAtEndOfLastFrame = didEndFrameIdle;
return hitFrame;
}
private static boolean hasEventBetweenTimestamps(
LongArray eventArray,
long startTime,
long endTime) {
for (int i = 0; i < eventArray.size(); i++) {
long time = eventArray.get(i);
if (time >= startTime && time < endTime) {
return true;
}
}
return false;
}
private static long getLastEventBetweenTimestamps(
LongArray eventArray,
long startTime,
long endTime) {
long lastEvent = -1;
for (int i = 0; i < eventArray.size(); i++) {
long time = eventArray.get(i);
if (time >= startTime && time < endTime) {
lastEvent = time;
} else if (time >= endTime) {
break;
}
}
return lastEvent;
}
private boolean didEndFrameIdle(long startTime, long endTime) {
long lastIdleTransition = getLastEventBetweenTimestamps(
mTransitionToIdleEvents,
startTime,
endTime);
long lastBusyTransition = getLastEventBetweenTimestamps(
mTransitionToBusyEvents,
startTime,
endTime);
if (lastIdleTransition == -1 && lastBusyTransition == -1) {
return mWasIdleAtEndOfLastFrame;
}
return lastIdleTransition > lastBusyTransition;
}
private static void cleanUp(LongArray eventArray, long endTime) {
int size = eventArray.size();
int indicesToRemove = 0;
for (int i = 0; i < size; i++) {
if (eventArray.get(i) < endTime) {
indicesToRemove++;
}
}
if (indicesToRemove > 0) {
for (int i = 0; i < size - indicesToRemove; i++) {
eventArray.set(i, eventArray.get(i + indicesToRemove));
}
eventArray.dropTail(indicesToRemove);
}
}
}

View File

@@ -0,0 +1,196 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.debug;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.TreeMap;
import android.annotation.TargetApi;
import android.view.Choreographer;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.infer.annotation.Assertions;
/**
* Each time a frame is drawn, records whether it should have expected any more callbacks since
* the last time a frame was drawn (i.e. was a frame skipped?). Uses this plus total elapsed time
* to determine FPS. Can also record total and expected frame counts, though NB, since the expected
* frame rate is estimated, the expected frame count will lose accuracy over time.
*
* Also records the JS FPS, i.e. the frames per second with which either JS updated the UI or was
* idle and not trying to update the UI. This is different from the FPS above since JS rendering is
* async.
*
* TargetApi 16 for use of Choreographer.
*/
@TargetApi(16)
public class FpsDebugFrameCallback implements Choreographer.FrameCallback {
public static class FpsInfo {
public final int totalFrames;
public final int totalJsFrames;
public final int totalExpectedFrames;
public final double fps;
public final double jsFps;
public final int totalTimeMs;
public FpsInfo(
int totalFrames,
int totalJsFrames,
int totalExpectedFrames,
double fps,
double jsFps,
int totalTimeMs) {
this.totalFrames = totalFrames;
this.totalJsFrames = totalJsFrames;
this.totalExpectedFrames = totalExpectedFrames;
this.fps = fps;
this.jsFps = jsFps;
this.totalTimeMs = totalTimeMs;
}
}
private static final double EXPECTED_FRAME_TIME = 16.9;
private final Choreographer mChoreographer;
private final ReactContext mReactContext;
private final UIManagerModule mUIManagerModule;
private final DidJSUpdateUiDuringFrameDetector mDidJSUpdateUiDuringFrameDetector;
private boolean mShouldStop = false;
private long mFirstFrameTime = -1;
private long mLastFrameTime = -1;
private int mNumFrameCallbacks = 0;
private int mNumFrameCallbacksWithBatchDispatches = 0;
private boolean mIsRecordingFpsInfoAtEachFrame = false;
private @Nullable TreeMap<Long, FpsInfo> mTimeToFps;
public FpsDebugFrameCallback(Choreographer choreographer, ReactContext reactContext) {
mChoreographer = choreographer;
mReactContext = reactContext;
mUIManagerModule = reactContext.getNativeModule(UIManagerModule.class);
mDidJSUpdateUiDuringFrameDetector = new DidJSUpdateUiDuringFrameDetector();
}
@Override
public void doFrame(long l) {
if (mShouldStop) {
return;
}
if (mFirstFrameTime == -1) {
mFirstFrameTime = l;
}
long lastFrameStartTime = mLastFrameTime;
mLastFrameTime = l;
if (mDidJSUpdateUiDuringFrameDetector.getDidJSHitFrameAndCleanup(
lastFrameStartTime,
l)) {
mNumFrameCallbacksWithBatchDispatches++;
}
mNumFrameCallbacks++;
if (mIsRecordingFpsInfoAtEachFrame) {
Assertions.assertNotNull(mTimeToFps);
FpsInfo info = new FpsInfo(
getNumFrames(),
getNumJSFrames(),
getExpectedNumFrames(),
getFPS(),
getJSFPS(),
getTotalTimeMS());
mTimeToFps.put(System.currentTimeMillis(), info);
}
mChoreographer.postFrameCallback(this);
}
public void start() {
mShouldStop = false;
mReactContext.getCatalystInstance().addBridgeIdleDebugListener(
mDidJSUpdateUiDuringFrameDetector);
mUIManagerModule.setUiManagerDebugListener(mDidJSUpdateUiDuringFrameDetector);
mChoreographer.postFrameCallback(this);
}
public void startAndRecordFpsAtEachFrame() {
mTimeToFps = new TreeMap<Long, FpsInfo>();
mIsRecordingFpsInfoAtEachFrame = true;
start();
}
public void stop() {
mShouldStop = true;
mReactContext.getCatalystInstance().removeBridgeIdleDebugListener(
mDidJSUpdateUiDuringFrameDetector);
mUIManagerModule.setUiManagerDebugListener(null);
}
public double getFPS() {
if (mLastFrameTime == mFirstFrameTime) {
return 0;
}
return ((double) (getNumFrames()) * 1e9) / (mLastFrameTime - mFirstFrameTime);
}
public double getJSFPS() {
if (mLastFrameTime == mFirstFrameTime) {
return 0;
}
return ((double) (getNumJSFrames()) * 1e9) / (mLastFrameTime - mFirstFrameTime);
}
public int getNumFrames() {
return mNumFrameCallbacks - 1;
}
public int getNumJSFrames() {
return mNumFrameCallbacksWithBatchDispatches - 1;
}
public int getExpectedNumFrames() {
double totalTimeMS = getTotalTimeMS();
int expectedFrames = (int) (totalTimeMS / EXPECTED_FRAME_TIME + 1);
return expectedFrames;
}
public int getTotalTimeMS() {
return (int) ((double) mLastFrameTime - mFirstFrameTime) / 1000000;
}
/**
* Returns the FpsInfo as if stop had been called at the given upToTimeMs. Only valid if
* monitoring was started with {@link #startAndRecordFpsAtEachFrame()}.
*/
public @Nullable FpsInfo getFpsInfo(long upToTimeMs) {
Assertions.assertNotNull(mTimeToFps, "FPS was not recorded at each frame!");
Map.Entry<Long, FpsInfo> bestEntry = mTimeToFps.floorEntry(upToTimeMs);
if (bestEntry == null) {
return null;
}
return bestEntry.getValue();
}
public void reset() {
mFirstFrameTime = -1;
mLastFrameTime = -1;
mNumFrameCallbacks = 0;
mNumFrameCallbacksWithBatchDispatches = 0;
mIsRecordingFpsInfoAtEachFrame = false;
mTimeToFps = null;
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.debug;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
/**
* Module that exposes the URL to the source code map (used for exception stack trace parsing) to JS
*/
public class SourceCodeModule extends BaseJavaModule {
private final String mSourceMapUrl;
private final String mSourceUrl;
public SourceCodeModule(String sourceUrl, String sourceMapUrl) {
mSourceMapUrl = sourceMapUrl;
mSourceUrl = sourceUrl;
}
@Override
public String getName() {
return "RKSourceCode";
}
@ReactMethod
public void getScriptText(final Callback onSuccess, final Callback onError) {
WritableMap map = new WritableNativeMap();
map.putString("fullSourceMappingURL", mSourceMapUrl);
onSuccess.invoke(map);
}
@Override
public @Nullable Map<String, Object> getConstants() {
HashMap<String, Object> constants = new HashMap<String, Object>();
constants.put("scriptURL", mSourceUrl);
return constants;
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.fresco;
import android.content.Context;
import com.facebook.cache.common.CacheKey;
import com.facebook.common.internal.AndroidPredicates;
import com.facebook.common.soloader.SoLoaderShim;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.backends.okhttp.OkHttpImagePipelineConfigFactory;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.facebook.imagepipeline.core.ImagePipelineFactory;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.modules.common.ModuleDataCleaner;
import com.facebook.react.modules.network.OkHttpClientProvider;
import com.facebook.soloader.SoLoader;
import com.squareup.okhttp.OkHttpClient;
/**
* Module to initialize the Fresco library.
*
* <p>Does not expose any methods to JavaScript code. For initialization and cleanup only.
*/
public class FrescoModule extends ReactContextBaseJavaModule implements
ModuleDataCleaner.Cleanable {
public FrescoModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public void initialize() {
super.initialize();
// Make sure the SoLoaderShim is configured to use our loader for native libraries.
// This code can be removed if using Fresco from Maven rather than from source
SoLoaderShim.setHandler(
new SoLoaderShim.Handler() {
@Override
public void loadLibrary(String libraryName) {
SoLoader.loadLibrary(libraryName);
}
});
Context context = this.getReactApplicationContext().getApplicationContext();
OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient();
ImagePipelineConfig config = OkHttpImagePipelineConfigFactory
.newBuilder(context, okHttpClient)
.setDownsampleEnabled(false)
.build();
Fresco.initialize(context, config);
}
@Override
public String getName() {
return "FrescoModule";
}
@Override
public void clearSensitiveData() {
// Clear image cache.
ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory();
imagePipelineFactory.getBitmapMemoryCache().removeAll(AndroidPredicates.<CacheKey>True());
imagePipelineFactory.getEncodedMemoryCache().removeAll(AndroidPredicates.<CacheKey>True());
imagePipelineFactory.getMainDiskStorageCache().clearAll();
imagePipelineFactory.getSmallImageDiskStorageCache().clearAll();
}
}

View File

@@ -0,0 +1,289 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.network;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.modules.network.OkHttpClientProvider;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.MultipartBuilder;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
/**
* Implements the XMLHttpRequest JavaScript interface.
*/
public final class NetworkingModule extends ReactContextBaseJavaModule {
private static final String CONTENT_ENCODING_HEADER_NAME = "content-encoding";
private static final String CONTENT_TYPE_HEADER_NAME = "content-type";
private static final String REQUEST_BODY_KEY_STRING = "string";
private static final String REQUEST_BODY_KEY_URI = "uri";
private static final String REQUEST_BODY_KEY_FORMDATA = "formData";
private static final String USER_AGENT_HEADER_NAME = "user-agent";
private final OkHttpClient mClient;
private final @Nullable String mDefaultUserAgent;
private boolean mShuttingDown;
/* package */ NetworkingModule(
ReactApplicationContext reactContext,
@Nullable String defaultUserAgent,
OkHttpClient client) {
super(reactContext);
mClient = client;
mShuttingDown = false;
mDefaultUserAgent = defaultUserAgent;
}
/**
* @param reactContext the ReactContext of the application
*/
public NetworkingModule(ReactApplicationContext reactContext) {
this(reactContext, null, OkHttpClientProvider.getOkHttpClient());
}
/**
* @param reactContext the ReactContext of the application
* @param defaultUserAgent the User-Agent header that will be set for all requests where the
* caller does not provide one explicitly
*/
public NetworkingModule(ReactApplicationContext reactContext, String defaultUserAgent) {
this(reactContext, defaultUserAgent, OkHttpClientProvider.getOkHttpClient());
}
@Override
public String getName() {
return "RCTNetworking";
}
@Override
public void onCatalystInstanceDestroy() {
mShuttingDown = true;
mClient.cancel(null);
}
@ReactMethod
public void sendRequest(
String method,
String url,
int requestId,
ReadableArray headers,
ReadableMap data,
final Callback callback) {
// We need to call the callback to avoid leaking memory on JS even when input for sending
// request is erroneous or insufficient. For non-http based failures we use code 0, which is
// interpreted as a transport error.
// Callback accepts following arguments: responseCode, headersString, responseBody
Request.Builder requestBuilder = new Request.Builder().url(url);
if (requestId != 0) {
requestBuilder.tag(requestId);
}
Headers requestHeaders = extractHeaders(headers, data);
if (requestHeaders == null) {
callback.invoke(0, null, "Unrecognized headers format");
return;
}
String contentType = requestHeaders.get(CONTENT_TYPE_HEADER_NAME);
String contentEncoding = requestHeaders.get(CONTENT_ENCODING_HEADER_NAME);
requestBuilder.headers(requestHeaders);
if (data == null) {
requestBuilder.method(method, null);
} else if (data.hasKey(REQUEST_BODY_KEY_STRING)) {
if (contentType == null) {
callback.invoke(0, null, "Payload is set but no content-type header specified");
return;
}
String body = data.getString(REQUEST_BODY_KEY_STRING);
MediaType contentMediaType = MediaType.parse(contentType);
if (RequestBodyUtil.isGzipEncoding(contentEncoding)) {
RequestBody requestBody = RequestBodyUtil.createGzip(contentMediaType, body);
if (requestBody == null) {
callback.invoke(0, null, "Failed to gzip request body");
return;
}
requestBuilder.method(method, requestBody);
} else {
requestBuilder.method(method, RequestBody.create(contentMediaType, body));
}
} else if (data.hasKey(REQUEST_BODY_KEY_URI)) {
if (contentType == null) {
callback.invoke(0, null, "Payload is set but no content-type header specified");
return;
}
String uri = data.getString(REQUEST_BODY_KEY_URI);
InputStream fileInputStream =
RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri);
if (fileInputStream == null) {
callback.invoke(0, null, "Could not retrieve file for uri " + uri);
return;
}
requestBuilder.method(
method,
RequestBodyUtil.create(MediaType.parse(contentType), fileInputStream));
} else if (data.hasKey(REQUEST_BODY_KEY_FORMDATA)) {
if (contentType == null) {
contentType = "multipart/form-data";
}
ReadableArray parts = data.getArray(REQUEST_BODY_KEY_FORMDATA);
MultipartBuilder multipartBuilder = constructMultipartBody(parts, contentType, callback);
if (multipartBuilder == null) {
return;
}
requestBuilder.method(method, multipartBuilder.build());
} else {
// Nothing in data payload, at least nothing we could understand anyway.
// Ignore and treat it as if it were null.
requestBuilder.method(method, null);
}
mClient.newCall(requestBuilder.build()).enqueue(
new com.squareup.okhttp.Callback() {
@Override
public void onFailure(Request request, IOException e) {
if (mShuttingDown) {
return;
}
// We need to call the callback to avoid leaking memory on JS even when input for
// sending request is erronous or insufficient. For non-http based failures we use
// code 0, which is interpreted as a transport error
callback.invoke(0, null, e.getMessage());
}
@Override
public void onResponse(Response response) throws IOException {
if (mShuttingDown) {
return;
}
// TODO(5472580) handle headers properly
String responseBody;
try {
responseBody = response.body().string();
} catch (IOException e) {
// The stream has been cancelled or closed, nothing we can do
callback.invoke(0, null, e.getMessage());
return;
}
callback.invoke(response.code(), null, responseBody);
}
});
}
@ReactMethod
public void abortRequest(final int requestId) {
// We have to use AsyncTask since this might trigger a NetworkOnMainThreadException, this is an
// open issue on OkHttp: https://github.com/square/okhttp/issues/869
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
mClient.cancel(requestId);
}
}.execute();
}
private @Nullable MultipartBuilder constructMultipartBody(
ReadableArray body,
String contentType,
Callback callback) {
MultipartBuilder multipartBuilder = new MultipartBuilder();
multipartBuilder.type(MediaType.parse(contentType));
for (int i = 0, size = body.size(); i < size; i++) {
ReadableMap bodyPart = body.getMap(i);
// Determine part's content type.
ReadableArray headersArray = bodyPart.getArray("headers");
Headers headers = extractHeaders(headersArray, null);
if (headers == null) {
callback.invoke(0, null, "Missing or invalid header format for FormData part.");
return null;
}
MediaType partContentType = null;
String partContentTypeStr = headers.get(CONTENT_TYPE_HEADER_NAME);
if (partContentTypeStr != null) {
partContentType = MediaType.parse(partContentTypeStr);
// Remove the content-type header because MultipartBuilder gets it explicitly as an
// argument and doesn't expect it in the headers array.
headers = headers.newBuilder().removeAll(CONTENT_TYPE_HEADER_NAME).build();
}
if (bodyPart.hasKey(REQUEST_BODY_KEY_STRING)) {
String bodyValue = bodyPart.getString(REQUEST_BODY_KEY_STRING);
multipartBuilder.addPart(headers, RequestBody.create(partContentType, bodyValue));
} else if (bodyPart.hasKey(REQUEST_BODY_KEY_URI)) {
if (partContentType == null) {
callback.invoke(0, null, "Binary FormData part needs a content-type header.");
return null;
}
String fileContentUriStr = bodyPart.getString(REQUEST_BODY_KEY_URI);
InputStream fileInputStream =
RequestBodyUtil.getFileInputStream(getReactApplicationContext(), fileContentUriStr);
if (fileInputStream == null) {
callback.invoke(0, null, "Could not retrieve file for uri " + fileContentUriStr);
return null;
}
multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream));
} else {
callback.invoke(0, null, "Unrecognized FormData part.");
}
}
return multipartBuilder;
}
/**
* Extracts the headers from the Array. If the format is invalid, this method will return null.
*/
private @Nullable Headers extractHeaders(
@Nullable ReadableArray headersArray,
@Nullable ReadableMap requestData) {
if (headersArray == null) {
return null;
}
Headers.Builder headersBuilder = new Headers.Builder();
for (int headersIdx = 0, size = headersArray.size(); headersIdx < size; headersIdx++) {
ReadableArray header = headersArray.getArray(headersIdx);
if (header == null || header.size() != 2) {
return null;
}
String headerName = header.getString(0);
String headerValue = header.getString(1);
headersBuilder.add(headerName, headerValue);
}
if (headersBuilder.get(USER_AGENT_HEADER_NAME) == null && mDefaultUserAgent != null) {
headersBuilder.add(USER_AGENT_HEADER_NAME, mDefaultUserAgent);
}
// Sanitize content encoding header, supported only when request specify payload as string
boolean isGzipSupported = requestData != null && requestData.hasKey(REQUEST_BODY_KEY_STRING);
if (!isGzipSupported) {
headersBuilder.removeAll(CONTENT_ENCODING_HEADER_NAME);
}
return headersBuilder.build();
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.network;
import java.util.concurrent.TimeUnit;
import com.squareup.okhttp.OkHttpClient;
/**
* Helper class that provides the same OkHttpClient instance that will be used for all networking
* requests.
*/
public class OkHttpClientProvider {
// Centralized OkHttpClient for all networking requests.
private static OkHttpClient sClient;
public static OkHttpClient getOkHttpClient() {
if (sClient == null) {
// TODO: #7108751 plug in stetho
sClient = new OkHttpClient();
// No timeouts by default
sClient.setConnectTimeout(0, TimeUnit.MILLISECONDS);
sClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
sClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
}
return sClient;
}
}

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.network;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;
import android.content.Context;
import android.net.Uri;
import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.internal.Util;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
/**
* Helper class that provides the necessary methods for creating the RequestBody from a file
* specification, such as a contentUri.
*/
/*package*/ class RequestBodyUtil {
private static final String CONTENT_ENCODING_GZIP = "gzip";
/**
* Returns whether encode type indicates the body needs to be gzip-ed.
*/
public static boolean isGzipEncoding(@Nullable final String encodingType) {
return CONTENT_ENCODING_GZIP.equalsIgnoreCase(encodingType);
}
/**
* Returns the input stream for a file given by its contentUri. Returns null if the file has not
* been found or if an error as occurred.
*/
public static @Nullable InputStream getFileInputStream(
Context context,
String fileContentUriStr) {
try {
Uri fileContentUri = Uri.parse(fileContentUriStr);
return context.getContentResolver().openInputStream(fileContentUri);
} catch (Exception e) {
FLog.e(
ReactConstants.TAG,
"Could not retrieve file for contentUri " + fileContentUriStr,
e);
return null;
}
}
/**
* Creates a RequestBody from a mediaType and gzip-ed body string
*/
public static @Nullable RequestBody createGzip(
final MediaType mediaType,
final String body) {
ByteArrayOutputStream gzipByteArrayOutputStream = new ByteArrayOutputStream();
try {
OutputStream gzipOutputStream = new GZIPOutputStream(gzipByteArrayOutputStream);
gzipOutputStream.write(body.getBytes());
gzipOutputStream.close();
} catch (IOException e) {
return null;
}
return RequestBody.create(mediaType, gzipByteArrayOutputStream.toByteArray());
}
/**
* Creates a RequestBody from a mediaType and inputStream given.
*/
public static RequestBody create(final MediaType mediaType, final InputStream inputStream) {
return new RequestBody() {
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public long contentLength() {
try {
return inputStream.available();
} catch (IOException e) {
return 0;
}
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
Source source = null;
try {
source = Okio.source(inputStream);
sink.writeAll(source);
} finally {
Util.closeQuietly(source);
}
}
};
}
}

View File

@@ -0,0 +1,147 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.storage;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Iterator;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import com.facebook.react.bridge.ReadableArray;
import org.json.JSONException;
import org.json.JSONObject;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN;
/**
* Helper for database operations.
*/
/* package */ class AsyncLocalStorageUtil {
/**
* Build the String required for an SQL select statement:
* WHERE key IN (?, ?, ..., ?)
* without 'WHERE' and with selectionCount '?'
*/
/* package */ static String buildKeySelection(int selectionCount) {
String[] list = new String[selectionCount];
Arrays.fill(list, "?");
return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")";
}
/**
* Build the String[] arguments needed for an SQL selection, i.e.:
* {a, b, c}
* to be used in the SQL select statement: WHERE key in (?, ?, ?)
*/
/* package */ static String[] buildKeySelectionArgs(ReadableArray keys) {
String[] selectionArgs = new String[keys.size()];
for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) {
selectionArgs[keyIndex] = keys.getString(keyIndex);
}
return selectionArgs;
}
/**
* Returns the value of the given key, or null if not found.
*/
/* package */ static @Nullable String getItemImpl(SQLiteDatabase db, String key) {
String[] columns = {VALUE_COLUMN};
String[] selectionArgs = {key};
Cursor cursor = db.query(
TABLE_CATALYST,
columns,
KEY_COLUMN + "=?",
selectionArgs,
null,
null,
null);
try {
if (!cursor.moveToFirst()) {
return null;
} else {
return cursor.getString(0);
}
} finally {
cursor.close();
}
}
/**
* Sets the value for the key given, returns true if successful, false otherwise.
*/
/* package */ static boolean setItemImpl(SQLiteDatabase db, String key, String value) {
ContentValues contentValues = new ContentValues();
contentValues.put(KEY_COLUMN, key);
contentValues.put(VALUE_COLUMN, value);
long inserted = db.insertWithOnConflict(
TABLE_CATALYST,
null,
contentValues,
SQLiteDatabase.CONFLICT_REPLACE);
return (-1 != inserted);
}
/**
* Does the actual merge of the (key, value) pair with the value stored in the database.
* NB: This assumes that a database lock is already in effect!
* @return the errorCode of the operation
*/
/* package */ static boolean mergeImpl(SQLiteDatabase db, String key, String value)
throws JSONException {
String oldValue = getItemImpl(db, key);
String newValue;
if (oldValue == null) {
newValue = value;
} else {
JSONObject oldJSON = new JSONObject(oldValue);
JSONObject newJSON = new JSONObject(value);
deepMergeInto(oldJSON, newJSON);
newValue = oldJSON.toString();
}
return setItemImpl(db, key, newValue);
}
/**
* Merges two {@link JSONObject}s. The newJSON object will be merged with the oldJSON object by
* either overriding its values, or merging them (if the values of the same key in both objects
* are of type {@link JSONObject}). oldJSON will contain the result of this merge.
*/
private static void deepMergeInto(JSONObject oldJSON, JSONObject newJSON)
throws JSONException {
Iterator<?> keys = newJSON.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
JSONObject newJSONObject = newJSON.optJSONObject(key);
JSONObject oldJSONObject = oldJSON.optJSONObject(key);
if (newJSONObject != null && oldJSONObject != null) {
deepMergeInto(oldJSONObject, newJSONObject);
oldJSON.put(key, oldJSONObject);
} else {
oldJSON.put(key, newJSON.get(key));
}
}
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.storage;
import javax.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
/**
* Helper class for database errors.
*/
public class AsyncStorageErrorUtil {
/**
* Create Error object to be passed back to the JS callback.
*/
/* package */ static WritableMap getError(@Nullable String key, String errorMessage) {
WritableMap errorMap = Arguments.createMap();
errorMap.putString("message", errorMessage);
if (key != null) {
errorMap.putString("key", key);
}
return errorMap;
}
/* package */ static WritableMap getInvalidKeyError(@Nullable String key) {
return getError(key, "Invalid key");
}
/* package */ static WritableMap getInvalidValueError(@Nullable String key) {
return getError(key, "Invalid Value");
}
/* package */ static WritableMap getDBError(@Nullable String key) {
return getError(key, "Database Error");
}
}

View File

@@ -0,0 +1,369 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.storage;
import javax.annotation.Nullable;
import java.util.HashSet;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.SetBuilder;
import com.facebook.react.modules.common.ModuleDataCleaner;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST;
import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN;
public final class AsyncStorageModule
extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable {
private @Nullable SQLiteDatabase mDb;
private boolean mShuttingDown = false;
public AsyncStorageModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "AsyncSQLiteDBStorage";
}
@Override
public void initialize() {
super.initialize();
mShuttingDown = false;
}
@Override
public void onCatalystInstanceDestroy() {
mShuttingDown = true;
if (mDb != null && mDb.isOpen()) {
mDb.close();
mDb = null;
}
}
@Override
public void clearSensitiveData() {
// Clear local storage. If fails, crash, since the app is potentially in a bad state and could
// cause a privacy violation. We're still not recovering from this well, but at least the error
// will be reported to the server.
clear(
new Callback() {
@Override
public void invoke(Object... args) {
if (args.length > 0) {
throw new RuntimeException("Clearing AsyncLocalStorage failed: " + args[0]);
}
FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage.");
}
});
}
/**
* Given an array of keys, this returns a map of (key, value) pairs for the keys found, and
* (key, null) for the keys that haven't been found.
*/
@ReactMethod
public void multiGet(final ReadableArray keys, final Callback callback) {
if (keys == null) {
callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null), null);
return;
}
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
if (!ensureDatabase()) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null), null);
return;
}
String[] columns = {KEY_COLUMN, VALUE_COLUMN};
HashSet<String> keysRemaining = SetBuilder.newHashSet();
WritableArray data = Arguments.createArray();
Cursor cursor = Assertions.assertNotNull(mDb).query(
TABLE_CATALYST,
columns,
AsyncLocalStorageUtil.buildKeySelection(keys.size()),
AsyncLocalStorageUtil.buildKeySelectionArgs(keys),
null,
null,
null);
try {
if (cursor.getCount() != keys.size()) {
// some keys have not been found - insert them with null into the final array
for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) {
keysRemaining.add(keys.getString(keyIndex));
}
}
if (cursor.moveToFirst()) {
do {
WritableArray row = Arguments.createArray();
row.pushString(cursor.getString(0));
row.pushString(cursor.getString(1));
data.pushArray(row);
keysRemaining.remove(cursor.getString(0));
} while (cursor.moveToNext());
}
} catch (Exception e) {
FLog.w(ReactConstants.TAG, "Exception in database multiGet ", e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null);
} finally {
cursor.close();
}
for (String key : keysRemaining) {
WritableArray row = Arguments.createArray();
row.pushString(key);
row.pushNull();
data.pushArray(row);
}
keysRemaining.clear();
callback.invoke(null, data);
}
}.execute();
}
/**
* Inserts multiple (key, value) pairs. If one or more of the pairs cannot be inserted, this will
* return AsyncLocalStorageFailure, but all other pairs will have been inserted.
* The insertion will replace conflicting (key, value) pairs.
*/
@ReactMethod
public void multiSet(final ReadableArray keyValueArray, final Callback callback) {
if (keyValueArray.size() == 0) {
callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null));
return;
}
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
if (!ensureDatabase()) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null));
return;
}
String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);";
SQLiteStatement statement = Assertions.assertNotNull(mDb).compileStatement(sql);
mDb.beginTransaction();
try {
for (int idx=0; idx < keyValueArray.size(); idx++) {
if (keyValueArray.getArray(idx).size() != 2) {
callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null));
return;
}
if (keyValueArray.getArray(idx).getString(0) == null) {
callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null));
return;
}
if (keyValueArray.getArray(idx).getString(1) == null) {
callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null));
return;
}
statement.clearBindings();
statement.bindString(1, keyValueArray.getArray(idx).getString(0));
statement.bindString(2, keyValueArray.getArray(idx).getString(1));
statement.execute();
}
mDb.setTransactionSuccessful();
} catch (Exception e) {
FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
} finally {
mDb.endTransaction();
}
callback.invoke();
}
}.execute();
}
/**
* Removes all rows of the keys given.
*/
@ReactMethod
public void multiRemove(final ReadableArray keys, final Callback callback) {
if (keys.size() == 0) {
callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null));
return;
}
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
if (!ensureDatabase()) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null));
return;
}
try {
Assertions.assertNotNull(mDb).delete(
TABLE_CATALYST,
AsyncLocalStorageUtil.buildKeySelection(keys.size()),
AsyncLocalStorageUtil.buildKeySelectionArgs(keys));
} catch (Exception e) {
FLog.w(ReactConstants.TAG, "Exception in database multiRemove ", e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
}
callback.invoke();
}
}.execute();
}
/**
* Given an array of (key, value) pairs, this will merge the given values with the stored values
* of the given keys, if they exist.
*/
@ReactMethod
public void multiMerge(final ReadableArray keyValueArray, final Callback callback) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
if (!ensureDatabase()) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null));
return;
}
Assertions.assertNotNull(mDb).beginTransaction();
try {
for (int idx = 0; idx < keyValueArray.size(); idx++) {
if (keyValueArray.getArray(idx).size() != 2) {
callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null));
return;
}
if (keyValueArray.getArray(idx).getString(0) == null) {
callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null));
return;
}
if (keyValueArray.getArray(idx).getString(1) == null) {
callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null));
return;
}
if (!AsyncLocalStorageUtil.mergeImpl(
mDb,
keyValueArray.getArray(idx).getString(0),
keyValueArray.getArray(idx).getString(1))) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null));
return;
}
}
mDb.setTransactionSuccessful();
} catch (Exception e) {
FLog.w(ReactConstants.TAG, e.getMessage(), e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
} finally {
mDb.endTransaction();
}
callback.invoke();
}
}.execute();
}
/**
* Clears the database.
*/
@ReactMethod
public void clear(final Callback callback) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
if (!ensureDatabase()) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null));
return;
}
try {
Assertions.assertNotNull(mDb).delete(TABLE_CATALYST, null, null);
} catch (Exception e) {
FLog.w(ReactConstants.TAG, "Exception in database clear ", e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
}
callback.invoke();
}
}.execute();
}
/**
* Returns an array with all keys from the database.
*/
@ReactMethod
public void getAllKeys(final Callback callback) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
if (!ensureDatabase()) {
callback.invoke(AsyncStorageErrorUtil.getDBError(null), null);
return;
}
WritableArray data = Arguments.createArray();
String[] columns = {KEY_COLUMN};
Cursor cursor = Assertions.assertNotNull(mDb)
.query(TABLE_CATALYST, columns, null, null, null, null, null);
try {
if (cursor.moveToFirst()) {
do {
data.pushString(cursor.getString(0));
} while (cursor.moveToNext());
}
} catch (Exception e) {
FLog.w(ReactConstants.TAG, "Exception in database getAllKeys ", e);
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null);
} finally {
cursor.close();
}
callback.invoke(null, data);
}
}.execute();
}
/**
* Verify the database exists and is open.
*/
private boolean ensureDatabase() {
if (mShuttingDown) {
return false;
}
if (mDb != null && mDb.isOpen()) {
return true;
}
mDb = initializeDatabase();
return true;
}
/**
* Create and/or open the database.
*/
private SQLiteDatabase initializeDatabase() {
CatalystSQLiteOpenHelper helperForDb =
new CatalystSQLiteOpenHelper(getReactApplicationContext());
return helperForDb.getWritableDatabase();
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.storage;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
// VisibleForTesting
public class CatalystSQLiteOpenHelper extends SQLiteOpenHelper {
// VisibleForTesting
public static final String DATABASE_NAME = "RKStorage";
static final int DATABASE_VERSION = 1;
static final String TABLE_CATALYST = "catalystLocalStorage";
static final String KEY_COLUMN = "key";
static final String VALUE_COLUMN = "value";
static final String VERSION_TABLE_CREATE =
"CREATE TABLE " + TABLE_CATALYST + " (" +
KEY_COLUMN + " TEXT PRIMARY KEY, " +
VALUE_COLUMN + " TEXT NOT NULL" +
")";
private Context mContext;
public CatalystSQLiteOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(VERSION_TABLE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO: t5494781 implement data migration
if (oldVersion != newVersion) {
mContext.deleteDatabase(DATABASE_NAME);
onCreate(db);
}
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.systeminfo;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import android.os.Build;
import com.facebook.react.bridge.BaseJavaModule;
/**
* Module that exposes Android Constants to JS.
*/
public class AndroidInfoModule extends BaseJavaModule {
@Override
public String getName() {
return "AndroidConstants";
}
@Override
public @Nullable Map<String, Object> getConstants() {
HashMap<String, Object> constants = new HashMap<String, Object>();
constants.put("Version", Build.VERSION.SDK_INT);
return constants;
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.toast;
import android.widget.Toast;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.common.MapBuilder;
import java.util.Map;
/**
* {@link NativeModule} that allows JS to show an Android Toast.
*/
public class ToastModule extends ReactContextBaseJavaModule {
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public ToastModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "ToastAndroid";
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = MapBuilder.newHashMap();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
}