Open souce the Android Dialog module

Summary:
public

The `DialogModule` requires `android.support.v4.app.FragmentManager` which means
every app that wants to use Dialogs would need to have its Activity extend the legacy
`android.support.v4.app.FragmentActivity`.

This diff makes the `DialogModule` work with both the Support `FragmentManager`
(for AdsManager & potentially other fb apps) and the `android.app.FragmentManager`
(for new apps with no legacy dependencies).

Also wrap the native module in the same `Alert` API that we have on iOS and provide
a cross-platform example. In my opinion the iOS Alert API is quite nice and easy to use.

We still keep `AlertIOS` around because of its `prompt` function which is iOS-specific
and also for backwards compatibility.

Reviewed By: foghina

Differential Revision: D2647000

fb-gh-sync-id: e2280451890bff58bd9c933ab53cd99055403858
This commit is contained in:
Martin Konicek
2015-12-18 12:03:29 +00:00
parent 9c034a9904
commit 0cd2f77116
13 changed files with 831 additions and 73 deletions
+137
View File
@@ -0,0 +1,137 @@
/**
* 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.
*
*/
'use strict';
var React = require('react-native');
var {
Alert,
Platform,
StyleSheet,
Text,
TouchableHighlight,
View,
} = React;
var UIExplorerBlock = require('./UIExplorerBlock');
// corporate ipsum > lorem ipsum
var alertMessage = 'Credibly reintermediate next-generation potentialities after goal-oriented ' +
'catalysts for change. Dynamically revolutionize.';
/**
* Simple alert examples.
*/
var SimpleAlertExampleBlock = React.createClass({
render: function() {
return (
<View>
<TouchableHighlight style={styles.wrapper}
onPress={() => Alert.alert(
'Alert Title',
alertMessage,
)}>
<View style={styles.button}>
<Text>Alert with message and default button</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => Alert.alert(
'Alert Title',
alertMessage,
[
{text: 'OK', onPress: () => console.log('OK Pressed!')},
]
)}>
<View style={styles.button}>
<Text>Alert with one button</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => Alert.alert(
'Alert Title',
alertMessage,
[
{text: 'Cancel', onPress: () => console.log('Cancel Pressed!')},
{text: 'OK', onPress: () => console.log('OK Pressed!')},
]
)}>
<View style={styles.button}>
<Text>Alert with two buttons</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => Alert.alert(
'Alert Title',
null,
[
{text: 'Foo', onPress: () => console.log('Foo Pressed!')},
{text: 'Bar', onPress: () => console.log('Bar Pressed!')},
{text: 'Baz', onPress: () => console.log('Baz Pressed!')},
]
)}>
<View style={styles.button}>
<Text>Alert with three buttons</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => Alert.alert(
'Foo Title',
alertMessage,
'..............'.split('').map((dot, index) => ({
text: 'Button ' + index,
onPress: () => console.log('Pressed ' + index)
}))
)}>
<View style={styles.button}>
<Text>Alert with too many buttons</Text>
</View>
</TouchableHighlight>
</View>
);
},
});
var AlertExample = React.createClass({
statics: {
title: 'Alert',
description: 'Alerts display a concise and informative message ' +
'and prompt the user to make a decision.',
},
render: function() {
return (
<UIExplorerBlock title={'Alert'}>
<SimpleAlertExampleBlock />
</UIExplorerBlock>
);
}
});
var styles = StyleSheet.create({
wrapper: {
borderRadius: 5,
marginBottom: 5,
},
button: {
backgroundColor: '#eeeeee',
padding: 10,
},
});
module.exports = {
AlertExample,
SimpleAlertExampleBlock,
}
+3 -65
View File
@@ -24,77 +24,15 @@ var {
AlertIOS,
} = React;
var { SimpleAlertExampleBlock } = require('./AlertExample');
exports.framework = 'React';
exports.title = 'AlertIOS';
exports.description = 'iOS alerts and action sheets';
exports.examples = [{
title: 'Alerts',
render() {
return (
<View>
<TouchableHighlight style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Foo Title',
'My Alert Msg'
)}>
<View style={styles.button}>
<Text>Alert with message and default button</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Foo Title',
null,
[
{text: 'Button', onPress: () => console.log('Button Pressed!')},
]
)}>
<View style={styles.button}>
<Text>Alert with only one button</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Foo Title',
'My Alert Msg',
[
{text: 'Foo', onPress: () => console.log('Foo Pressed!')},
{text: 'Bar', onPress: () => console.log('Bar Pressed!')},
]
)}>
<View style={styles.button}>
<Text>Alert with two buttons</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Foo Title',
null,
[
{text: 'Foo', onPress: () => console.log('Foo Pressed!')},
{text: 'Bar', onPress: () => console.log('Bar Pressed!')},
{text: 'Baz', onPress: () => console.log('Baz Pressed!')},
]
)}>
<View style={styles.button}>
<Text>Alert with 3 buttons</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={() => AlertIOS.alert(
'Foo Title',
'My Alert Msg',
'..............'.split('').map((dot, index) => ({
text: 'Button ' + index,
onPress: () => console.log('Pressed ' + index)
}))
)}>
<View style={styles.button}>
<Text>Alert with too many buttons</Text>
</View>
</TouchableHighlight>
</View>
);
return <SimpleAlertExampleBlock />;
}
},
{
@@ -38,6 +38,7 @@ var COMPONENTS = [
var APIS = [
require('./AccessibilityAndroidExample.android'),
require('./AlertExample').AlertExample,
require('./BorderExample'),
require('./ClipboardExample'),
require('./GeolocationExample'),
+126
View File
@@ -0,0 +1,126 @@
/**
* 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 Alert
* @flow
*/
'use strict';
var AlertIOS = require('AlertIOS');
var Platform = require('Platform');
var DialogModuleAndroid = require('NativeModules').DialogManagerAndroid;
import type { AlertType, AlertButtonStyle } from 'AlertIOS';
type Buttons = Array<{
text?: string;
onPress?: ?Function;
style?: AlertButtonStyle;
}>;
/**
* Launches an alert dialog with the specified title and message.
*
* Optionally provide a list of buttons. Tapping any button will fire the
* respective onPress callback and dismiss the alert. By default, the only
* button will be an 'OK' button.
*
* The last button in the list will be considered the 'Primary' button.
*
* ## iOS
*
* On iOS you can specify any number of buttons. Each button can optionally
* specify a style and you can also specify type of the alert.
* Refer to `AlertIOS` for details.
*
* ## Android
*
* On Android at most three buttons can be specified. Android has a concept
* of a 'neutral', 'negative' and a 'positive' button:
*
* - If you specify one button, it will be the 'positive' one (such as 'OK')
* - Two buttons mean 'negative', 'positive' (such as 'Cancel', 'OK')
* - Three buttons mean 'neutral', 'negative', 'positive' (such as 'Later', 'Cancel', 'OK')
*
* ```
* Alert.alert(
* 'Alert Title',
* 'My Alert Msg',
* [
* {text: 'Ask me later', onPress: () => console.log('Ask me later pressed')},
* {text: 'Cancel', onPress: () => console.log('Cancel Pressed')},
* {text: 'OK', onPress: () => console.log('OK Pressed')},
* ]
* )
* ```
*/
class Alert {
static alert(
title: ?string,
message?: ?string,
buttons?: Buttons,
type?: AlertType
): void {
if (Platform.OS === 'ios') {
AlertIOS.alert(title, message, buttons, type);
} else if (Platform.OS === 'android') {
AlertAndroid.alert(title, message, buttons);
}
}
}
/**
* Wrapper around the Android native module.
*/
class AlertAndroid {
static alert(
title: ?string,
message?: ?string,
buttons?: Buttons,
): void {
var config = {
title: title || '',
message: message || '',
};
// At most three buttons (neutral, negative, positive). Ignore rest.
// The text 'OK' should be probably localized. iOS Alert does that in native.
var validButtons: Buttons = buttons ? buttons.slice(0, 3) : [{text: 'OK'}];
var buttonPositive = validButtons.pop();
var buttonNegative = validButtons.pop();
var buttonNeutral = validButtons.pop();
if (buttonNeutral) {
config = {...config, buttonNeutral: buttonNeutral.text || '' }
}
if (buttonNegative) {
config = {...config, buttonNegative: buttonNegative.text || '' }
}
if (buttonPositive) {
config = {...config, buttonPositive: buttonPositive.text || '' }
}
DialogModuleAndroid.showAlert(
config,
(errorMessage) => console.warn(message),
(action, buttonKey) => {
if (action !== DialogModuleAndroid.buttonClicked) {
return;
}
if (buttonKey === DialogModuleAndroid.buttonNeutral) {
buttonNeutral.onPress && buttonNeutral.onPress();
} else if (buttonKey === DialogModuleAndroid.buttonNegative) {
buttonNegative.onPress && buttonNegative.onPress();
} else if (buttonKey === DialogModuleAndroid.buttonPositive) {
buttonPositive.onPress && buttonPositive.onPress();
}
}
);
}
}
module.exports = Alert;
+5 -6
View File
@@ -14,14 +14,14 @@
var RCTAlertManager = require('NativeModules').AlertManager;
var invariant = require('invariant');
type AlertType = $Enum<{
export type AlertType = $Enum<{
'default': string;
'plain-text': string;
'secure-text': string;
'login-password': string;
}>;
type AlertButtonStyle = $Enum<{
export type AlertButtonStyle = $Enum<{
'default': string;
'cancel': string;
'destructive': string;
@@ -32,20 +32,19 @@ type AlertButtonStyle = $Enum<{
*
* Optionally provide a list of buttons. Tapping any button will fire the
* respective onPress callback and dismiss the alert. By default, the only
* button will be an 'OK' button
* button will be an 'OK' button.
*
* ```
* AlertIOS.alert(
* 'Foo Title',
* 'My Alert Msg',
* [
* {text: 'OK', onPress: () => console.log('OK Pressed!')},
* {text: 'Cancel', onPress: () => console.log('Cancel Pressed!'), style: 'cancel'},
* {text: 'OK', onPress: () => console.log('OK Pressed')},
* {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
* ]
* )
* ```
*/
class AlertIOS {
static alert(
title: ?string,
+1
View File
@@ -51,6 +51,7 @@ var ReactNative = {
// APIs
get ActionSheetIOS() { return require('ActionSheetIOS'); },
get AdSupportIOS() { return require('AdSupportIOS'); },
get Alert() { return require('Alert'); },
get AlertIOS() { return require('AlertIOS'); },
get Animated() { return require('Animated'); },
get AppRegistry() { return require('AppRegistry'); },
+1 -1
View File
@@ -263,7 +263,7 @@ dependencies {
testCompile "org.powermock:powermock-classloading-xstream:${POWERMOCK_VERSION}"
testCompile "org.mockito:mockito-core:${MOCKITO_CORE_VERSION}"
testCompile "org.easytesting:fest-assert-core:${FEST_ASSERT_CORE_VERSION}"
testCompile("org.robolectric:robolectric:${ROBOLECTRIC_VERSION}")
testCompile "org.robolectric:robolectric:${ROBOLECTRIC_VERSION}"
}
apply from: 'release.gradle'
@@ -184,7 +184,7 @@ public abstract class ReactInstanceManager {
/**
* Path to the JS bundle file to be loaded from the file system.
*
* Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle}
* Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle"}
*/
public Builder setJSBundleFile(String jsBundleFile) {
mJSBundleFile = jsBundleFile;
@@ -0,0 +1,77 @@
/**
* 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.dialog;
import javax.annotation.Nullable;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.content.Context;
import android.os.Bundle;
/**
* A fragment used to display the dialog.
*/
/* package */ class AlertFragment extends DialogFragment implements DialogInterface.OnClickListener {
/* package */ static final String ARG_TITLE = "title";
/* package */ static final String ARG_MESSAGE = "message";
/* package */ static final String ARG_BUTTON_POSITIVE = "button_positive";
/* package */ static final String ARG_BUTTON_NEGATIVE = "button_negative";
/* package */ static final String ARG_BUTTON_NEUTRAL = "button_neutral";
private final @Nullable DialogModule.AlertFragmentListener mListener;
public AlertFragment(@Nullable DialogModule.AlertFragmentListener listener, Bundle arguments) {
mListener = listener;
setArguments(arguments);
}
public static Dialog createDialog(
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
AlertDialog.Builder builder = new AlertDialog.Builder(activityContext)
.setTitle(arguments.getString(ARG_TITLE))
.setMessage(arguments.getString(ARG_MESSAGE));
if (arguments.containsKey(ARG_BUTTON_POSITIVE)) {
builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment);
}
if (arguments.containsKey(ARG_BUTTON_NEGATIVE)) {
builder.setNegativeButton(arguments.getString(ARG_BUTTON_NEGATIVE), fragment);
}
if (arguments.containsKey(ARG_BUTTON_NEUTRAL)) {
builder.setNeutralButton(arguments.getString(ARG_BUTTON_NEUTRAL), fragment);
}
return builder.create();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return createDialog(getActivity(), getArguments(), this);
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (mListener != null) {
mListener.onClick(dialog, which);
}
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mListener != null) {
mListener.onDismiss(dialog);
}
}
}
@@ -0,0 +1,255 @@
/**
* 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.dialog;
import javax.annotation.Nullable;
import java.util.Map;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
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.ReadableMap;
import com.facebook.react.common.MapBuilder;
public class DialogModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
/* package */ static final String FRAGMENT_TAG =
"com.facebook.catalyst.react.dialog.DialogModule";
/* package */ static final String NAME = "DialogManagerAndroid";
/* package */ static final String ACTION_BUTTON_CLICKED = "buttonClicked";
/* package */ static final String ACTION_DISMISSED = "dismissed";
/* package */ static final String KEY_TITLE = "title";
/* package */ static final String KEY_MESSAGE = "message";
/* package */ static final String KEY_BUTTON_POSITIVE = "buttonPositive";
/* package */ static final String KEY_BUTTON_NEGATIVE = "buttonNegative";
/* package */ static final String KEY_BUTTON_NEUTRAL = "buttonNeutral";
/* package */ static final Map<String, Object> CONSTANTS = MapBuilder.<String, Object>of(
ACTION_BUTTON_CLICKED, ACTION_BUTTON_CLICKED,
ACTION_DISMISSED, ACTION_DISMISSED,
KEY_BUTTON_POSITIVE, DialogInterface.BUTTON_POSITIVE,
KEY_BUTTON_NEGATIVE, DialogInterface.BUTTON_NEGATIVE,
KEY_BUTTON_NEUTRAL, DialogInterface.BUTTON_NEUTRAL);
private boolean mIsInForeground;
public DialogModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return NAME;
}
/**
* Helper to allow this module to work with both the standard FragmentManager
* and the Support FragmentManager (for apps that need to use it for legacy reasons).
* Since the two APIs don't share a common interface there's unfortunately some
* code duplication.
*/
private class FragmentManagerHelper {
// Exactly one of the two is null
private final @Nullable android.app.FragmentManager mFragmentManager;
private final @Nullable android.support.v4.app.FragmentManager mSupportFragmentManager;
private @Nullable Object mFragmentToShow;
private boolean isUsingSupportLibrary() {
return mSupportFragmentManager != null;
}
public FragmentManagerHelper(android.support.v4.app.FragmentManager supportFragmentManager) {
mFragmentManager = null;
mSupportFragmentManager = supportFragmentManager;
}
public FragmentManagerHelper(android.app.FragmentManager fragmentManager) {
mFragmentManager = fragmentManager;
mSupportFragmentManager = null;
}
public void showPendingAlert() {
if (mFragmentToShow == null) {
return;
}
if (isUsingSupportLibrary()) {
((SupportAlertFragment) mFragmentToShow).show(mSupportFragmentManager, FRAGMENT_TAG);
} else {
((AlertFragment) mFragmentToShow).show(mFragmentManager, FRAGMENT_TAG);
}
mFragmentToShow = null;
}
private void dismissExisting() {
if (isUsingSupportLibrary()) {
SupportAlertFragment oldFragment =
(SupportAlertFragment) mSupportFragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (oldFragment != null) {
oldFragment.dismiss();
}
} else {
AlertFragment oldFragment =
(AlertFragment) mFragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (oldFragment != null) {
oldFragment.dismiss();
}
}
}
public void showNewAlert(boolean isInForeground, Bundle arguments, Callback actionCallback) {
dismissExisting();
AlertFragmentListener actionListener =
actionCallback != null ? new AlertFragmentListener(actionCallback) : null;
if (isUsingSupportLibrary()) {
SupportAlertFragment alertFragment = new SupportAlertFragment(actionListener, arguments);
if (isInForeground) {
alertFragment.show(mSupportFragmentManager, FRAGMENT_TAG);
} else {
mFragmentToShow = alertFragment;
}
} else {
AlertFragment alertFragment = new AlertFragment(actionListener, arguments);
if (isInForeground) {
alertFragment.show(mFragmentManager, FRAGMENT_TAG);
} else {
mFragmentToShow = alertFragment;
}
}
}
}
/* package */ class AlertFragmentListener implements OnClickListener, OnDismissListener {
private final Callback mCallback;
private boolean mCallbackConsumed = false;
public AlertFragmentListener(Callback callback) {
mCallback = callback;
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (!mCallbackConsumed) {
if (getReactApplicationContext().hasActiveCatalystInstance()) {
mCallback.invoke(ACTION_BUTTON_CLICKED, which);
mCallbackConsumed = true;
}
}
}
@Override
public void onDismiss(DialogInterface dialog) {
if (!mCallbackConsumed) {
if (getReactApplicationContext().hasActiveCatalystInstance()) {
mCallback.invoke(ACTION_DISMISSED);
mCallbackConsumed = true;
}
}
}
}
@Override
public Map<String, Object> getConstants() {
return CONSTANTS;
}
@Override
public void initialize() {
getReactApplicationContext().addLifecycleEventListener(this);
}
@Override
public void onHostPause() {
// Don't show the dialog if the host is paused.
mIsInForeground = false;
}
@Override
public void onHostDestroy() {
}
@Override
public void onHostResume() {
mIsInForeground = true;
// Check if a dialog has been created while the host was paused, so that we can show it now.
FragmentManagerHelper fragmentManagerHelper = getFragmentManagerHelper();
Assertions.assertNotNull(
fragmentManagerHelper,
"Attached DialogModule to host with pending alert but no FragmentManager " +
"(not attached to an Activity).");
fragmentManagerHelper.showPendingAlert();
}
@ReactMethod
public void showAlert(
ReadableMap options,
Callback errorCallback,
Callback actionCallback) {
FragmentManagerHelper fragmentManagerHelper = getFragmentManagerHelper();
if (fragmentManagerHelper == null) {
errorCallback.invoke("Tried to show an alert while not attached to an Activity");
return;
}
final Bundle args = new Bundle();
if (options.hasKey(KEY_TITLE)) {
args.putString(AlertFragment.ARG_TITLE, options.getString(KEY_TITLE));
}
if (options.hasKey(KEY_MESSAGE)) {
args.putString(AlertFragment.ARG_MESSAGE, options.getString(KEY_MESSAGE));
}
if (options.hasKey(KEY_BUTTON_POSITIVE)) {
args.putString(AlertFragment.ARG_BUTTON_POSITIVE, options.getString(KEY_BUTTON_POSITIVE));
}
if (options.hasKey(KEY_BUTTON_NEGATIVE)) {
args.putString(AlertFragment.ARG_BUTTON_NEGATIVE, options.getString(KEY_BUTTON_NEGATIVE));
}
if (options.hasKey(KEY_BUTTON_NEUTRAL)) {
args.putString(AlertFragment.ARG_BUTTON_NEUTRAL, options.getString(KEY_BUTTON_NEUTRAL));
}
fragmentManagerHelper.showNewAlert(mIsInForeground, args, actionCallback);
}
/**
* Creates a new helper to work with either the FragmentManager or the legacy support
* FragmentManager transparently. Returns null if we're not attached to an Activity.
*
* DO NOT HOLD LONG-LIVED REFERENCES TO THE OBJECT RETURNED BY THIS METHOD, AS THIS WILL CAUSE
* MEMORY LEAKS.
*/
private @Nullable FragmentManagerHelper getFragmentManagerHelper() {
Activity activity = getCurrentActivity();
if (activity == null) {
return null;
}
if (activity instanceof FragmentActivity) {
return new FragmentManagerHelper(((FragmentActivity) activity).getSupportFragmentManager());
} else {
return new FragmentManagerHelper(activity.getFragmentManager());
}
}
}
@@ -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.dialog;
import javax.annotation.Nullable;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
/**
* {@link AlertFragment} for apps that use the Support FragmentActivity and FragmentManager
* for legacy reasons.
*/
/* package */ class SupportAlertFragment extends DialogFragment implements DialogInterface.OnClickListener {
private final @Nullable DialogModule.AlertFragmentListener mListener;
public SupportAlertFragment(@Nullable DialogModule.AlertFragmentListener listener, Bundle arguments) {
mListener = listener;
setArguments(arguments);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return AlertFragment.createDialog(getActivity(), getArguments(), this);
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (mListener != null) {
mListener.onClick(dialog, which);
}
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mListener != null) {
mListener.onDismiss(dialog);
}
}
}
@@ -17,6 +17,7 @@ import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.modules.dialog.DialogModule;
import com.facebook.react.modules.fresco.FrescoModule;
import com.facebook.react.modules.intent.IntentModule;
import com.facebook.react.modules.location.LocationModule;
@@ -53,6 +54,7 @@ public class MainReactPackage implements ReactPackage {
return Arrays.<NativeModule>asList(
new AsyncStorageModule(reactContext),
new ClipboardModule(reactContext),
new DialogModule(reactContext),
new FrescoModule(reactContext),
new IntentModule(reactContext),
new LocationModule(reactContext),
@@ -0,0 +1,170 @@
/**
* 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.dialog;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.app.Activity;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.SimpleMap;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.util.ActivityController;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
public class DialogModuleTest {
private ActivityController<Activity> mActivityController;
private Activity mActivity;
private DialogModule mDialogModule;
final static class SimpleCallback implements Callback {
private Object[] mArgs;
private int mCalls;
@Override
public void invoke(Object... args) {
mCalls++;
mArgs = args;
}
public int getCalls() {
return mCalls;
}
public Object[] getArgs() {
return mArgs;
}
}
@Before
public void setUp() throws Exception {
mActivityController = Robolectric.buildActivity(Activity.class);
mActivity = mActivityController
.create()
.start()
.resume()
.get();
final ReactApplicationContext context = PowerMockito.mock(ReactApplicationContext.class);
PowerMockito.when(context.hasActiveCatalystInstance()).thenReturn(true);
PowerMockito.when(context, "getCurrentActivity").thenReturn(mActivity);
mDialogModule = new DialogModule(context);
mDialogModule.onHostResume();
}
@After
public void tearDown() {
mActivityController.pause().stop().destroy();
mActivityController = null;
mDialogModule = null;
}
@Test
public void testAllOptions() {
final SimpleMap options = new SimpleMap();
options.putString("title", "Title");
options.putString("message", "Message");
options.putString("buttonPositive", "OK");
options.putString("buttonNegative", "Cancel");
options.putString("buttonNeutral", "Later");
mDialogModule.showAlert(options, null, null);
final AlertFragment fragment = getFragment();
assertNotNull("Fragment was not displayed", fragment);
final AlertDialog dialog = (AlertDialog) fragment.getDialog();
assertEquals("OK", dialog.getButton(DialogInterface.BUTTON_POSITIVE).getText().toString());
assertEquals("Cancel", dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getText().toString());
assertEquals("Later", dialog.getButton(DialogInterface.BUTTON_NEUTRAL).getText().toString());
}
@Test
public void testCallbackPositive() {
final SimpleMap options = new SimpleMap();
options.putString("buttonPositive", "OK");
final SimpleCallback actionCallback = new SimpleCallback();
mDialogModule.showAlert(options, null, actionCallback);
final AlertDialog dialog = (AlertDialog) getFragment().getDialog();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
assertEquals(1, actionCallback.getCalls());
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.getArgs()[0]);
assertEquals(DialogInterface.BUTTON_POSITIVE, actionCallback.getArgs()[1]);
}
@Test
public void testCallbackNegative() {
final SimpleMap options = new SimpleMap();
options.putString("buttonNegative", "Cancel");
final SimpleCallback actionCallback = new SimpleCallback();
mDialogModule.showAlert(options, null, actionCallback);
final AlertDialog dialog = (AlertDialog) getFragment().getDialog();
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();
assertEquals(1, actionCallback.getCalls());
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.getArgs()[0]);
assertEquals(DialogInterface.BUTTON_NEGATIVE, actionCallback.getArgs()[1]);
}
@Test
public void testCallbackNeutral() {
final SimpleMap options = new SimpleMap();
options.putString("buttonNeutral", "Later");
final SimpleCallback actionCallback = new SimpleCallback();
mDialogModule.showAlert(options, null, actionCallback);
final AlertDialog dialog = (AlertDialog) getFragment().getDialog();
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).performClick();
assertEquals(1, actionCallback.getCalls());
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.getArgs()[0]);
assertEquals(DialogInterface.BUTTON_NEUTRAL, actionCallback.getArgs()[1]);
}
@Test
public void testCallbackDismiss() {
final SimpleMap options = new SimpleMap();
final SimpleCallback actionCallback = new SimpleCallback();
mDialogModule.showAlert(options, null, actionCallback);
getFragment().getDialog().dismiss();
assertEquals(1, actionCallback.getCalls());
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.getArgs()[0]);
}
private AlertFragment getFragment() {
return (AlertFragment) mActivity.getFragmentManager()
.findFragmentByTag(DialogModule.FRAGMENT_TAG);
}
}