diff --git a/Examples/UIExplorer/AppStateExample.js b/Examples/UIExplorer/AppStateExample.js new file mode 100644 index 00000000000000..07d70be82de7c1 --- /dev/null +++ b/Examples/UIExplorer/AppStateExample.js @@ -0,0 +1,80 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule AppStateExample + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + AppState, + Text, + View +} = React; + +var AppStateSubscription = React.createClass({ + getInitialState() { + return { + appState: AppState.currentState, + previousAppStates: [], + }; + }, + componentDidMount: function() { + AppState.addEventListener('change', this._handleAppStateChange); + }, + componentWillUnmount: function() { + AppState.removeEventListener('change', this._handleAppStateChange); + }, + _handleAppStateChange: function(appState) { + var previousAppStates = this.state.previousAppStates.slice(); + previousAppStates.push(this.state.appState); + this.setState({ + appState, + previousAppStates, + }); + }, + render() { + if (this.props.showCurrentOnly) { + return ( + + {this.state.appState} + + ); + } + return ( + + {JSON.stringify(this.state.previousAppStates)} + + ); + } +}); + +exports.title = 'AppState'; +exports.description = 'app background status'; +exports.examples = [ + { + title: 'AppState.currentState', + description: 'Can be null on app initialization', + render() { return {AppState.currentState}; } + }, + { + title: 'Subscribed AppState:', + description: 'This changes according to the current state, so you can only ever see it rendered as "active"', + render(): ReactElement { return ; } + }, + { + title: 'Previous states:', + render(): ReactElement { return ; } + }, +]; diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 81442e0b982e24..85fc1d116cdd5b 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -42,6 +42,7 @@ var COMPONENTS = [ var APIS = [ require('./AccessibilityAndroidExample.android'), require('./AlertExample').AlertExample, + require('./AppStateExample'), require('./BorderExample'), require('./ClipboardExample'), require('./GeolocationExample'), diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 1d96b2d49ce848..9d04c2b7ad64ae 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -64,6 +64,7 @@ var APIS = [ require('./AnimatedExample'), require('./AnimatedGratuitousApp/AnExApp'), require('./AppStateIOSExample'), + require('./AppStateExample'), require('./AsyncStorageExample'), require('./BorderExample'), require('./CameraRollExample.ios'), diff --git a/Libraries/AppState/AppState.js b/Libraries/AppState/AppState.js new file mode 100644 index 00000000000000..1ccaf46c8d2395 --- /dev/null +++ b/Libraries/AppState/AppState.js @@ -0,0 +1,144 @@ +/** + * 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. + * + * @providesModule AppState + * @flow + */ +'use strict'; + +var Map = require('Map'); +var NativeModules = require('NativeModules'); +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTAppState = NativeModules.AppState; + +var logError = require('logError'); +var invariant = require('invariant'); + +var _eventHandlers = { + change: new Map(), + memoryWarning: new Map(), +}; + +/** + * `AppState` can tell you if the app is in the foreground or background, + * and notify you when the state changes. + * + * AppState is frequently used to determine the intent and proper behavior when + * handling push notifications. + * + * ### App States + * + * - `active` - The app is running in the foreground + * - `background` - The app is running in the background. The user is either + * in another app or on the home screen + * - `inactive` - This is a transition state that currently never happens for + * typical React Native apps. + * + * For more information, see + * [Apple's documentation](https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html) + * + * ### Basic Usage + * + * To see the current state, you can check `AppState.currentState`, which + * will be kept up-to-date. However, `currentState` will be null at launch + * while `AppState` retrieves it over the bridge. + * + * ``` + * getInitialState: function() { + * return { + * currentAppState: AppState.currentState, + * }; + * }, + * componentDidMount: function() { + * AppState.addEventListener('change', this._handleAppStateChange); + * }, + * componentWillUnmount: function() { + * AppState.removeEventListener('change', this._handleAppStateChange); + * }, + * _handleAppStateChange: function(currentAppState) { + * this.setState({ currentAppState, }); + * }, + * render: function() { + * return ( + * Current state is: {this.state.currentAppState} + * ); + * }, + * ``` + * + * This example will only ever appear to say "Current state is: active" because + * the app is only visible to the user when in the `active` state, and the null + * state will happen only momentarily. + */ + +var AppState = { + + /** + * Add a handler to AppState changes by listening to the `change` event type + * and providing the handler + */ + addEventListener: function( + type: string, + handler: Function + ) { + invariant( + ['change', 'memoryWarning'].indexOf(type) !== -1, + 'Trying to subscribe to unknown event: "%s"', type + ); + if (type === 'change') { + _eventHandlers[type].set(handler, RCTDeviceEventEmitter.addListener( + 'appStateDidChange', + (appStateData) => { + handler(appStateData.app_state); + } + )); + } else if (type === 'memoryWarning') { + _eventHandlers[type].set(handler, RCTDeviceEventEmitter.addListener( + 'memoryWarning', + handler + )); + } + }, + + /** + * Remove a handler by passing the `change` event type and the handler + */ + removeEventListener: function( + type: string, + handler: Function + ) { + invariant( + ['change', 'memoryWarning'].indexOf(type) !== -1, + 'Trying to remove listener for unknown event: "%s"', type + ); + if (!_eventHandlers[type].has(handler)) { + return; + } + _eventHandlers[type].get(handler).remove(); + _eventHandlers[type].delete(handler); + }, + + // TODO: getCurrentAppState callback seems to be called at a really late stage + // after app launch. Trying to get currentState when mounting App component + // will likely to have the initial value here. + // Initialize to 'active' instead of null. + currentState: ('active' : ?string), + +}; + +RCTDeviceEventEmitter.addListener( + 'appStateDidChange', + (appStateData) => { + AppState.currentState = appStateData.app_state; + } +); + +RCTAppState.getCurrentAppState().then((appStateData) => { + AppState.currentState = appStateData.app_state; +}).catch(logError) + +module.exports = AppState; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index b28f255d9c35ff..b502bd97620115 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -60,6 +60,7 @@ var ReactNative = { get Animated() { return require('Animated'); }, get AppRegistry() { return require('AppRegistry'); }, get AppStateIOS() { return require('AppStateIOS'); }, + get AppState() { return require('AppState'); }, get AsyncStorage() { return require('AsyncStorage'); }, get BackAndroid() { return require('BackAndroid'); }, get CameraRoll() { return require('CameraRoll'); }, diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/appstate/AppStateModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/appstate/AppStateModule.java new file mode 100644 index 00000000000000..466652da4ebc8f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/appstate/AppStateModule.java @@ -0,0 +1,74 @@ +/** + * 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.appstate; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; + +import static com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; + +public class AppStateModule extends ReactContextBaseJavaModule + implements LifecycleEventListener { + + private String mAppState = "active"; + + public AppStateModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "AppState"; + } + + @Override + public void initialize() { + getReactApplicationContext().addLifecycleEventListener(this); + } + + @ReactMethod + public void getCurrentAppState(Promise promise) { + promise.resolve(createAppStateEventMap()); + } + + @Override + public void onHostResume() { + mAppState = "active"; + sendAppStateEvent(); + } + + @Override + public void onHostPause() { + mAppState = "background"; + sendAppStateEvent(); + } + + @Override + public void onHostDestroy() { + + } + + private WritableMap createAppStateEventMap() { + WritableMap appState = Arguments.createMap(); + appState.putString("app_state", mAppState); + return appState; + } + + private void sendAppStateEvent() { + getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) + .emit("appStateDidChange", createAppStateEventMap()); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index e636cd1c08a532..5e415de7eb0b8d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -25,6 +25,7 @@ import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.toast.ToastModule; +import com.facebook.react.modules.appstate.AppStateModule; import com.facebook.react.modules.websocket.WebSocketModule; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.art.ARTRenderableViewManager; @@ -64,6 +65,7 @@ public List createNativeModules(ReactApplicationContext reactConte new LocationModule(reactContext), new NetworkingModule(reactContext), new NetInfoModule(reactContext), + new AppStateModule(reactContext), new WebSocketModule(reactContext), new ToastModule(reactContext)); } diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 52ff2b8ae7806e..e6c0504de2b28b 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -224,6 +224,7 @@ var apis = [ '../Libraries/Animated/src/AnimatedImplementation.js', '../Libraries/AppRegistry/AppRegistry.js', '../Libraries/AppStateIOS/AppStateIOS.ios.js', + '../Libraries/AppState/AppState.js', '../Libraries/Storage/AsyncStorage.js', '../Libraries/Utilities/BackAndroid.android.js', '../Libraries/CameraRoll/CameraRoll.js',