diff --git a/Examples/UIExplorer/DatePickerAndroidExample.js b/Examples/UIExplorer/DatePickerAndroidExample.js new file mode 100644 index 000000000..e59517d91 --- /dev/null +++ b/Examples/UIExplorer/DatePickerAndroidExample.js @@ -0,0 +1,117 @@ +/** + * 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 { + DatePickerAndroid, + StyleSheet, + Text, + TouchableWithoutFeedback, +} = React; + +var UIExplorerBlock = require('./UIExplorerBlock'); +var UIExplorerPage = require('./UIExplorerPage'); + +var DatePickerAndroidExample = React.createClass({ + + statics: { + title: 'DatePickerAndroid', + description: 'Standard Android date picker dialog', + }, + + getInitialState() { + return { + presetDate: new Date(2020, 4, 5), + allDate: new Date(2020, 4, 5), + simpleText: 'pick a date', + minText: 'pick a date, no earlier than today', + maxText: 'pick a date, no later than today', + presetText: 'pick a date, preset to 2020/5/5', + allText: 'pick a date between 2020/5/1 and 2020/5/10', + }; + }, + + async showPicker(stateKey, options) { + try { + var newState = {}; + const {action, year, month, day} = await DatePickerAndroid.open(options); + if (action === DatePickerAndroid.dismissedAction) { + newState[stateKey + 'Text'] = 'dismissed'; + } else { + var date = new Date(year, month, day); + newState[stateKey + 'Text'] = date.toLocaleDateString(); + newState[stateKey + 'Date'] = date; + } + this.setState(newState); + } catch ({code, message}) { + console.warn(`Error in example '${stateKey}': `, message); + } + }, + + render() { + return ( + + + + {this.state.simpleText} + + + + + {this.state.presetText} + + + + + {this.state.minText} + + + + + {this.state.maxText} + + + + + {this.state.allText} + + + + ); + }, +}); + +var styles = StyleSheet.create({ + text: { + color: 'black', + }, +}); + +module.exports = DatePickerAndroidExample; diff --git a/Examples/UIExplorer/ListViewExample.js b/Examples/UIExplorer/ListViewExample.js index c4df7c7a9..ef16151ed 100644 --- a/Examples/UIExplorer/ListViewExample.js +++ b/Examples/UIExplorer/ListViewExample.js @@ -30,7 +30,7 @@ var UIExplorerPage = require('./UIExplorerPage'); var ListViewSimpleExample = React.createClass({ statics: { - title: ' - Simple', + title: '', description: 'Performant, scrollable list of data.' }, @@ -50,7 +50,7 @@ var ListViewSimpleExample = React.createClass({ render: function() { return ( - Simple'} + title={this.props.navigator ? null : ''} noSpacer={true} noScroll={true}> ', + description: 'Provides multiple options to choose from, using either a dropdown menu or a dialog.', + }, + getInitialState: function() { return { selected1: 'key1', @@ -124,19 +130,11 @@ const PickerAndroidExample = React.createClass({ this.setState({mode: newMode}); }, - onSelect: function(key, value) { + onSelect: function(key: string, value: string) { const newState = {}; newState[key] = value; this.setState(newState); }, }); -exports.title = ''; -exports.displayName = 'PickerAndroidExample'; -exports.description = 'The Android Picker component provides multiple options to choose from'; -exports.examples = [ - { - title: 'PickerAndroidExample', - render(): ReactElement { return ; } - }, -]; +module.exports = PickerAndroidExample; diff --git a/Examples/UIExplorer/RefreshControlExample.js b/Examples/UIExplorer/RefreshControlExample.js index b8feefb66..f56c9fed5 100644 --- a/Examples/UIExplorer/RefreshControlExample.js +++ b/Examples/UIExplorer/RefreshControlExample.js @@ -69,7 +69,7 @@ const RefreshControlExample = React.createClass({ isRefreshing: false, loaded: 0, rowData: Array.from(new Array(20)).map( - (val, i) => ({text: 'Initial row' + i, clicks: 0})), + (val, i) => ({text: 'Initial row ' + i, clicks: 0})), }; }, @@ -108,7 +108,7 @@ const RefreshControlExample = React.createClass({ // prepend 10 items const rowData = Array.from(new Array(10)) .map((val, i) => ({ - text: 'Loaded row' + (+this.state.loaded + i), + text: 'Loaded row ' + (+this.state.loaded + i), clicks: 0, })) .concat(this.state.rowData); diff --git a/Examples/UIExplorer/TimePickerAndroidExample.js b/Examples/UIExplorer/TimePickerAndroidExample.js new file mode 100644 index 000000000..06733fc35 --- /dev/null +++ b/Examples/UIExplorer/TimePickerAndroidExample.js @@ -0,0 +1,112 @@ +/** + * 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 { + TimePickerAndroid, + StyleSheet, + Text, + TouchableWithoutFeedback, +} = React; + +var UIExplorerBlock = require('./UIExplorerBlock'); +var UIExplorerPage = require('./UIExplorerPage'); + +var TimePickerAndroidExample = React.createClass({ + + statics: { + title: 'TimePickerAndroid', + description: 'Standard Android time picker dialog', + }, + + getInitialState() { + // *Text, *Hour and *Minute are set by successCallback -- this updates the text with the time + // picked by the user and makes it so the next time they open it the hour and minute they picked + // before is displayed. + return { + isoFormatText: 'pick a time (24-hour format)', + presetHour: 4, + presetMinute: 4, + presetText: 'pick a time, default: 4:04AM', + simpleText: 'pick a time', + }; + }, + + async showPicker(stateKey, options) { + try { + const {action, minute, hour} = await TimePickerAndroid.open(options); + var newState = {}; + if (action === TimePickerAndroid.timeSetAction) { + newState[stateKey + 'Text'] = _formatTime(hour, minute); + newState[stateKey + 'Hour'] = hour; + newState[stateKey + 'Minute'] = minute; + } else if (action === TimePickerAndroid.dismissedAction) { + newState[stateKey + 'Text'] = 'dismissed'; + } + this.setState(newState); + } catch ({code, message}) { + console.warn(`Error in example '${stateKey}': `, message); + } + }, + + render() { + return ( + + + + {this.state.simpleText} + + + + + {this.state.presetText} + + + + + + {this.state.isoFormatText} + + + + ); + }, +}); + +/** + * Returns e.g. '3:05'. + */ +function _formatTime(hour, minute) { + return hour + ':' + (minute < 10 ? '0' + minute : minute); +} + +var styles = StyleSheet.create({ + text: { + color: 'black', + }, +}); + +module.exports = TimePickerAndroidExample; + diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 01e8a331e..66caeef51 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -47,6 +47,7 @@ var APIS = [ require('./BorderExample'), require('./CameraRollExample'), require('./ClipboardExample'), + require('./DatePickerAndroidExample'), require('./GeolocationExample'), require('./IntentAndroidExample.android'), require('./LayoutEventsExample'), @@ -54,6 +55,7 @@ var APIS = [ require('./NetInfoExample'), require('./PanResponderExample'), require('./PointerEventsExample'), + require('./TimePickerAndroidExample'), require('./TimerExample'), require('./ToastAndroidExample.android'), require('./XHRExample'), diff --git a/Libraries/Components/Clipboard/Clipboard.js b/Libraries/Components/Clipboard/Clipboard.js index 9b3ef5950..b09bc5b2f 100644 --- a/Libraries/Components/Clipboard/Clipboard.js +++ b/Libraries/Components/Clipboard/Clipboard.js @@ -26,10 +26,10 @@ module.exports = { */ getString() { if (arguments.length > 0) { - let callback = arguments[0]; - console.warn('Clipboard.getString(callback) is deprecated. Use the returned Promise instead'); - Clipboard.getString().then(callback); - return; + let callback = arguments[0]; + console.warn('Clipboard.getString(callback) is deprecated. Use the returned Promise instead'); + Clipboard.getString().then(callback); + return; } return Clipboard.getString(); }, @@ -40,7 +40,7 @@ module.exports = { * Clipboard.setString('hello world'); * } * ``` - * @param this parameter is content that will be set into clipboard. + * @param the content to be stored in the clipboard. */ setString(content) { Clipboard.setString(content); diff --git a/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js new file mode 100644 index 000000000..d446544c6 --- /dev/null +++ b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js @@ -0,0 +1,84 @@ +/** + * 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 DatePickerAndroid + * @flow + */ +'use strict'; + +const DatePickerModule = require('NativeModules').DatePickerAndroid; + +/** + * Convert a Date to a timestamp. + */ +function _toMillis(options: Object, key: string) { + const dateVal = options[key]; + // Is it a Date object? + if (typeof dateVal === 'object' && typeof dateVal.getMonth === 'function') { + options[key] = dateVal.getTime(); + } +} + +/** + * Opens the standard Android date picker dialog. + * + * ### Example + * + * ``` + * try { + * const {action, year, month, day} = await DatePickerAndroid.open({ + * // Use `new Date()` for current date. + * // May 25 2020. Month 0 is January. + * date: new Date(2020, 4, 25) + * }); + * if (action !== DatePickerAndroid.dismissedAction) { + * // Selected year, month (0-11), day + * } + * } catch ({code, message}) { + * console.warn('Cannot open date picker', message); + * } + * ``` + */ +class DatePickerAndroid { + /** + * Opens the standard Android date picker dialog. + * + * The available keys for the `options` object are: + * * `date` (`Date` object or timestamp in milliseconds) - date to show by default + * * `minDate` (`Date` or timestamp in milliseconds) - minimum date that can be selected + * * `maxDate` (`Date` object or timestamp in milliseconds) - minimum date that can be selected + * + * Returns a Promise which will be invoked an object containing `action`, `year`, `month` (0-11), + * `day` if the user picked a date. If the user dismissed the dialog, the Promise will + * still be resolved with action being `DatePickerAndroid.dismissedAction` and all the other keys + * being undefined. **Always** check whether the `action` before reading the values. + * + * Note the native date picker dialog has some UI glitches on Android 4 and lower + * when using the `minDate` and `maxDate` options. + */ + static async open(options: Object): Promise { + let optionsMs = options; + if (optionsMs) { + _toMillis(options, 'date'); + _toMillis(options, 'minDate'); + _toMillis(options, 'maxDate'); + } + return DatePickerModule.open(options); + } + + /** + * A date has been selected. + */ + static get dateSetAction() { return 'dateSetAction'; } + /** + * The dialog has been dismissed. + */ + static get dismissedAction() { return 'dismissedAction'; } +} + +module.exports = DatePickerAndroid; diff --git a/Libraries/Components/DatePickerAndroid/DatePickerAndroid.ios.js b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.ios.js new file mode 100644 index 000000000..b0ab643fa --- /dev/null +++ b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.ios.js @@ -0,0 +1,24 @@ +/** + * 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 DatePickerAndroid + * @flow + */ +'use strict'; + +var warning = require('warning'); + +const DatePickerAndroid = { + async open(options: Object): Promise { + return Promise.reject({ + message: 'DatePickerAndroid is not supported on this platform.' + }); + }, +} + +module.exports = DatePickerAndroid; diff --git a/Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js b/Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js new file mode 100644 index 000000000..98bf034a8 --- /dev/null +++ b/Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js @@ -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. + * + * @providesModule TimePickerAndroid + * @flow + */ +'use strict'; + +const TimePickerModule = require('NativeModules').TimePickerAndroid; + +/** + * Opens the standard Android time picker dialog. + * + * ### Example + * + * ``` + * try { + * const {action, hour, minute} = await TimePickerAndroid.open({ + * hour: 14, + * minute: 0, + * is24Hour: false, // Will display '2 PM' + * }); + * if (action !== DatePickerAndroid.dismissedAction) { + * // Selected hour (0-23), minute (0-59) + * } + * } catch ({code, message}) { + * console.warn('Cannot open time picker', message); + * } + * ``` + */ +class TimePickerAndroid { + + /** + * Opens the standard Android time picker dialog. + * + * The available keys for the `options` object are: + * * `hour` (0-23) - the hour to show, defaults to the current time + * * `minute` (0-59) - the minute to show, defaults to the current time + * * `is24Hour` (boolean) - If `true`, the picker uses the 24-hour format. If `false`, + * the picker shows an AM/PM chooser. If undefined, the default for the current locale + * is used. + * + * Returns a Promise which will be invoked an object containing `action`, `hour` (0-23), + * `minute` (0-59) if the user picked a time. If the user dismissed the dialog, the Promise will + * still be resolved with action being `TimePickerAndroid.dismissedAction` and all the other keys + * being undefined. **Always** check whether the `action` before reading the values. + */ + static async open(options: Object): Promise { + return TimePickerModule.open(options); + } + + /** + * A time has been selected. + */ + static get timeSetAction() { return 'timeSetAction'; } + /** + * The dialog has been dismissed. + */ + static get dismissedAction() { return 'dismissedAction'; } +}; + +module.exports = TimePickerAndroid; diff --git a/Libraries/Components/TimePickerAndroid/TimePickerAndroid.ios.js b/Libraries/Components/TimePickerAndroid/TimePickerAndroid.ios.js new file mode 100644 index 000000000..df993ba92 --- /dev/null +++ b/Libraries/Components/TimePickerAndroid/TimePickerAndroid.ios.js @@ -0,0 +1,24 @@ +/** + * 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 TimePickerAndroid + * @flow + */ +'use strict'; + +var warning = require('warning'); + +const TimePickerAndroid = { + async open(options: Object): Promise { + return Promise.reject({ + message: 'TimePickerAndroid is not supported on this platform.' + }); + }, +} + +module.exports = TimePickerAndroid; diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.ios.js b/Libraries/Components/ToastAndroid/ToastAndroid.ios.js index 087786e08..8eb79dfb0 100644 --- a/Libraries/Components/ToastAndroid/ToastAndroid.ios.js +++ b/Libraries/Components/ToastAndroid/ToastAndroid.ios.js @@ -19,7 +19,7 @@ var ToastAndroid = { message: string, duration: number ): void { - warning(false, 'Cannot use ToastAndroid on iOS.'); + warning(false, 'ToastAndroid is not supported on this platform.'); }, }; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index ade020c53..865ee0cab 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -66,6 +66,7 @@ var ReactNative = { get BackAndroid() { return require('BackAndroid'); }, get CameraRoll() { return require('CameraRoll'); }, get Clipboard() { return require('Clipboard'); }, + get DatePickerAndroid() { return require('DatePickerAndroid'); }, get Dimensions() { return require('Dimensions'); }, get Easing() { return require('Easing'); }, get ImagePickerIOS() { return require('ImagePickerIOS'); }, @@ -80,6 +81,7 @@ var ReactNative = { get Settings() { return require('Settings'); }, get StatusBarIOS() { return require('StatusBarIOS'); }, get StyleSheet() { return require('StyleSheet'); }, + get TimePickerAndroid() { return require('TimePickerAndroid'); }, get UIManager() { return require('UIManager'); }, get VibrationIOS() { return require('VibrationIOS'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 618eb2e05..17d46a0af 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -78,6 +78,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { BackAndroid: require('BackAndroid'), CameraRoll: require('CameraRoll'), Clipboard: require('Clipboard'), + DatePickerAndroid: require('DatePickerAndroid'), Dimensions: require('Dimensions'), Easing: require('Easing'), ImagePickerIOS: require('ImagePickerIOS'), @@ -92,6 +93,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { Settings: require('Settings'), StatusBarIOS: require('StatusBarIOS'), StyleSheet: require('StyleSheet'), + TimePickerAndroid: require('TimePickerAndroid'), UIManager: require('UIManager'), VibrationIOS: require('VibrationIOS'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java index 5b107fd99..87a9b4b53 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java @@ -9,8 +9,6 @@ package com.facebook.react.bridge; -import javax.annotation.Nullable; - /** * Interface that represents a JavaScript Promise which can be passed to the native module as a * method parameter. @@ -19,10 +17,38 @@ import javax.annotation.Nullable; * will be marked as "remoteAsync" and will return a promise when invoked from JavaScript. */ public interface Promise { + + /** + * Successfully resolve the Promise. + */ void resolve(Object value); - void reject(Throwable reason); + + /** + * Report an error which wasn't caused by an exception. + */ + void reject(String code, String message); + + /** + * Report an exception. + */ + void reject(String code, Throwable e); + + /** + * Report an exception with a custom error message. + */ + void reject(String code, String message, Throwable e); + + /** + * Report an error which wasn't caused by an exception. + * @deprecated Prefer passing a module-specific error code to JS. + * Using this method will pass the error code "EUNSPECIFIED". + */ @Deprecated - void reject(String reason); - void reject(String code, Throwable extra); - void reject(String code, String reason, @Nullable Throwable extra); + void reject(String message); + + /** + * Report an exception, with default error code. + * Useful in catch-all scenarios where it's unclear why the error occurred. + */ + void reject(Throwable reason); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java index 0c1c85c9e..519448f6d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java @@ -34,23 +34,28 @@ public class PromiseImpl implements Promise { } @Override - public void reject(Throwable reason) { - reject(DEFAULT_ERROR, reason.getMessage(), reason); + public void reject(String code, String message) { + reject(code, message, /*Throwable*/null); } @Override @Deprecated - public void reject(String reason) { - reject(DEFAULT_ERROR, reason, null); + public void reject(String message) { + reject(DEFAULT_ERROR, message, /*Throwable*/null); } @Override - public void reject(String code, Throwable extra) { - reject(code, extra.getMessage(), extra); + public void reject(String code, Throwable e) { + reject(code, e.getMessage(), e); } @Override - public void reject(String code, String reason, @Nullable Throwable extra) { + public void reject(Throwable e) { + reject(DEFAULT_ERROR, e.getMessage(), e); + } + + @Override + public void reject(String code, String message, @Nullable Throwable e) { if (mReject != null) { if (code == null) { code = DEFAULT_ERROR; @@ -60,7 +65,7 @@ public class PromiseImpl implements Promise { // error instance. WritableNativeMap errorInfo = new WritableNativeMap(); errorInfo.putString("code", code); - errorInfo.putString("message", reason); + errorInfo.putString("message", message); // TODO(8850038): add the stack trace info in, need to figure out way to serialize that mReject.invoke(errorInfo); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java b/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java index 357586aa1..e5302355c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java @@ -59,7 +59,6 @@ import com.facebook.react.common.ReactConstants; */ public class CameraRollManager extends ReactContextBaseJavaModule { - private static final String TAG = "Catalyst/CameraRollManager"; private static final String ERROR_UNABLE_TO_LOAD = "E_UNABLE_TO_LOAD"; private static final String ERROR_UNABLE_TO_LOAD_PERMISSION = "E_UNABLE_TO_LOAD_PERMISSION"; private static final String ERROR_UNABLE_TO_SAVE = "E_UNABLE_TO_SAVE"; @@ -145,7 +144,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule { Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); pictures.mkdirs(); if (!pictures.isDirectory()) { - mPromise.reject(ERROR_UNABLE_TO_LOAD, "External storage pictures directory not available", null); + mPromise.reject(ERROR_UNABLE_TO_LOAD, "External storage pictures directory not available"); return; } File dest = new File(pictures, source.getName()); @@ -178,7 +177,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule { if (uri != null) { mPromise.resolve(uri.toString()); } else { - mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery", null); + mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery"); } } }); @@ -302,7 +301,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule { Images.Media.DATE_TAKEN + " DESC, " + Images.Media.DATE_MODIFIED + " DESC LIMIT " + (mFirst + 1)); // set LIMIT to first + 1 so that we know how to populate page_info if (photos == null) { - mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos", null); + mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos"); } else { try { putEdges(resolver, photos, response, mFirst); @@ -412,7 +411,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule { width = options.outWidth; height = options.outHeight; } catch (IOException e) { - FLog.e(TAG, "Could not get width/height for " + photoUri.toString(), e); + FLog.e(ReactConstants.TAG, "Could not get width/height for " + photoUri.toString(), e); return false; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/BUCK new file mode 100644 index 000000000..d53004753 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/BUCK @@ -0,0 +1,20 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'datepicker', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_dep('third-party/android/support/v4:lib-support-v4'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':datepicker', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DatePickerDialogFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DatePickerDialogFragment.java new file mode 100644 index 000000000..6568f3504 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DatePickerDialogFragment.java @@ -0,0 +1,103 @@ +/** + * 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.datepicker; + +import javax.annotation.Nullable; + +import java.util.Calendar; + +import android.annotation.SuppressLint; +import android.app.DatePickerDialog; +import android.app.DatePickerDialog.OnDateSetListener; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.widget.DatePicker; + +@SuppressLint("ValidFragment") +public class DatePickerDialogFragment extends DialogFragment { + + /** + * Minimum date supported by {@link DatePicker}, 01 Jan 1900 + */ + private static final long DEFAULT_MIN_DATE = -2208988800001l; + + @Nullable + private OnDateSetListener mOnDateSetListener; + @Nullable + private OnDismissListener mOnDismissListener; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + return createDialog(args, getActivity(), mOnDateSetListener); + } + + /*package*/ static Dialog createDialog( + Bundle args, Context activityContext, @Nullable OnDateSetListener onDateSetListener) { + final Calendar c = Calendar.getInstance(); + if (args != null && args.containsKey(DatePickerDialogModule.ARG_DATE)) { + c.setTimeInMillis(args.getLong(DatePickerDialogModule.ARG_DATE)); + } + final int year = c.get(Calendar.YEAR); + final int month = c.get(Calendar.MONTH); + final int day = c.get(Calendar.DAY_OF_MONTH); + + final DatePickerDialog dialog = + new DismissableDatePickerDialog(activityContext, onDateSetListener, year, month, day); + final DatePicker datePicker = dialog.getDatePicker(); + + if (args != null && args.containsKey(DatePickerDialogModule.ARG_MINDATE)) { + // Set minDate to the beginning of the day. We need this because of clowniness in datepicker + // that causes it to throw an exception if minDate is greater than the internal timestamp + // that it generates from the y/m/d passed in the constructor. + c.setTimeInMillis(args.getLong(DatePickerDialogModule.ARG_MINDATE)); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + datePicker.setMinDate(c.getTimeInMillis()); + } else { + // This is to work around a bug in DatePickerDialog where it doesn't display a title showing + // the date under certain conditions. + datePicker.setMinDate(DEFAULT_MIN_DATE); + } + if (args != null && args.containsKey(DatePickerDialogModule.ARG_MAXDATE)) { + // Set maxDate to the end of the day, same reason as for minDate. + c.setTimeInMillis(args.getLong(DatePickerDialogModule.ARG_MAXDATE)); + c.set(Calendar.HOUR_OF_DAY, 23); + c.set(Calendar.MINUTE, 59); + c.set(Calendar.SECOND, 59); + c.set(Calendar.MILLISECOND, 999); + datePicker.setMaxDate(c.getTimeInMillis()); + } + + return dialog; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (mOnDismissListener != null) { + mOnDismissListener.onDismiss(dialog); + } + } + + /*package*/ void setOnDateSetListener(@Nullable OnDateSetListener onDateSetListener) { + mOnDateSetListener = onDateSetListener; + } + + /*package*/ void setOnDismissListener(@Nullable OnDismissListener onDismissListener) { + mOnDismissListener = onDismissListener; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DatePickerDialogModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DatePickerDialogModule.java new file mode 100644 index 000000000..6f76ea7e4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DatePickerDialogModule.java @@ -0,0 +1,176 @@ +/** + * 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.datepicker; + +import javax.annotation.Nullable; + +import java.util.Map; + +import android.app.Activity; +import android.app.DatePickerDialog.OnDateSetListener; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.widget.DatePicker; + +import com.facebook.react.bridge.NativeModule; +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.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.annotations.VisibleForTesting; + +/** + * {@link NativeModule} that allows JS to show a native date picker dialog and get called back when + * the user selects a date. + */ +public class DatePickerDialogModule extends ReactContextBaseJavaModule { + + @VisibleForTesting + public static final String FRAGMENT_TAG = "DatePickerAndroid"; + + private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY"; + + /* package */ static final String ARG_DATE = "date"; + /* package */ static final String ARG_MINDATE = "minDate"; + /* package */ static final String ARG_MAXDATE = "maxDate"; + + /* package */ static final String ACTION_DATE_SET = "dateSetAction"; + /* package */ static final String ACTION_DISMISSED = "dismissedAction"; + + public DatePickerDialogModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "DatePickerAndroid"; + } + + private class DatePickerDialogListener implements OnDateSetListener, OnDismissListener { + + private final Promise mPromise; + private boolean mPromiseResolved = false; + + public DatePickerDialogListener(final Promise promise) { + mPromise = promise; + } + + @Override + public void onDateSet(DatePicker view, int year, int month, int day) { + if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) { + WritableMap result = new WritableNativeMap(); + result.putString("action", ACTION_DATE_SET); + result.putInt("year", year); + result.putInt("month", month); + result.putInt("day", day); + mPromise.resolve(result); + mPromiseResolved = true; + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) { + WritableMap result = new WritableNativeMap(); + result.putString("action", ACTION_DISMISSED); + mPromise.resolve(result); + mPromiseResolved = true; + } + } + } + + /** + * Show a date picker dialog. + * + * @param options a map containing options. Available keys are: + * + * + * {@code date} (timestamp in milliseconds) the date to show by default + * + * {@code minDate} (timestamp in milliseconds) the minimum date the user should be allowed + * to select + * + * + * {@code maxDate} (timestamp in milliseconds) the maximum date the user should be allowed + * to select + * + * + * + * @param promise This will be invoked with parameters action, year, + * month (0-11), day, where action is {@code dateSetAction} or + * {@code dismissedAction}, depending on what the user did. If the action is + * dismiss, year, month and date are undefined. + */ + @ReactMethod + public void open(@Nullable final ReadableMap options, Promise promise) { + Activity activity = getCurrentActivity(); + if (activity == null) { + promise.reject( + ERROR_NO_ACTIVITY, + "Tried to open a DatePicker dialog while not attached to an Activity"); + return; + } + // We want to support both android.app.Activity and the pre-Honeycomb FragmentActivity + // (for apps that use it for legacy reasons). This unfortunately leads to some code duplication. + if (activity instanceof android.support.v4.app.FragmentActivity) { + android.support.v4.app.FragmentManager fragmentManager = + ((android.support.v4.app.FragmentActivity) activity).getSupportFragmentManager(); + android.support.v4.app.DialogFragment oldFragment = + (android.support.v4.app.DialogFragment)fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (oldFragment != null) { + oldFragment.dismiss(); + } + SupportDatePickerDialogFragment fragment = new SupportDatePickerDialogFragment(); + if (options != null) { + final Bundle args = createFragmentArguments(options); + fragment.setArguments(args); + } + final DatePickerDialogListener listener = new DatePickerDialogListener(promise); + fragment.setOnDismissListener(listener); + fragment.setOnDateSetListener(listener); + fragment.show(fragmentManager, FRAGMENT_TAG); + } else { + FragmentManager fragmentManager = activity.getFragmentManager(); + DialogFragment oldFragment = (DialogFragment)fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (oldFragment != null) { + oldFragment.dismiss(); + } + DatePickerDialogFragment fragment = new DatePickerDialogFragment(); + if (options != null) { + final Bundle args = createFragmentArguments(options); + fragment.setArguments(args); + } + final DatePickerDialogListener listener = new DatePickerDialogListener(promise); + fragment.setOnDismissListener(listener); + fragment.setOnDateSetListener(listener); + fragment.show(fragmentManager, FRAGMENT_TAG); + } + } + + private Bundle createFragmentArguments(ReadableMap options) { + final Bundle args = new Bundle(); + if (options.hasKey(ARG_DATE) && !options.isNull(ARG_DATE)) { + args.putLong(ARG_DATE, (long) options.getDouble(ARG_DATE)); + } + if (options.hasKey(ARG_MINDATE) && !options.isNull(ARG_MINDATE)) { + args.putLong(ARG_MINDATE, (long) options.getDouble(ARG_MINDATE)); + } + if (options.hasKey(ARG_MAXDATE) && !options.isNull(ARG_MAXDATE)) { + args.putLong(ARG_MAXDATE, (long) options.getDouble(ARG_MAXDATE)); + } + return args; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DismissableDatePickerDialog.java b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DismissableDatePickerDialog.java new file mode 100644 index 000000000..57791948e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/DismissableDatePickerDialog.java @@ -0,0 +1,57 @@ +/** + * 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.datepicker; + +import javax.annotation.Nullable; + +import android.app.DatePickerDialog; +import android.content.Context; +import android.os.Build; + +/** + * + * Certain versions of Android (Jellybean-KitKat) have a bug where when dismissed, the + * {@link DatePickerDialog} still calls the OnDateSetListener. This class works around that issue. + * + * + * + * See: Issue 34833 + * + */ +public class DismissableDatePickerDialog extends DatePickerDialog { + + public DismissableDatePickerDialog( + Context context, + @Nullable OnDateSetListener callback, + int year, + int monthOfYear, + int dayOfMonth) { + super(context, callback, year, monthOfYear, dayOfMonth); + } + + public DismissableDatePickerDialog( + Context context, + int theme, + @Nullable OnDateSetListener callback, + int year, + int monthOfYear, + int dayOfMonth) { + super(context, theme, callback, year, monthOfYear, dayOfMonth); + } + + @Override + protected void onStop() { + // do *not* call super.onStop() on KitKat on lower, as that would erroneously call the + // OnDateSetListener when the dialog is dismissed, or call it twice when "OK" is pressed. + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + super.onStop(); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/SupportDatePickerDialogFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/SupportDatePickerDialogFragment.java new file mode 100644 index 000000000..d935d58d8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/datepicker/SupportDatePickerDialogFragment.java @@ -0,0 +1,51 @@ +/** + * 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.datepicker; + +import javax.annotation.Nullable; + +import android.annotation.SuppressLint; +import android.app.DatePickerDialog.OnDateSetListener; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; + +@SuppressLint("ValidFragment") +public class SupportDatePickerDialogFragment extends DialogFragment { + + @Nullable + private OnDateSetListener mOnDateSetListener; + @Nullable + private OnDismissListener mOnDismissListener; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle args = getArguments(); + return DatePickerDialogFragment.createDialog(args, getActivity(), mOnDateSetListener); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (mOnDismissListener != null) { + mOnDismissListener.onDismiss(dialog); + } + } + + /*package*/ void setOnDateSetListener(@Nullable OnDateSetListener onDateSetListener) { + mOnDateSetListener = onDateSetListener; + } + + /*package*/ void setOnDismissListener(@Nullable OnDismissListener onDismissListener) { + mOnDismissListener = onDismissListener; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/BUCK new file mode 100644 index 000000000..1b7d3eac3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/BUCK @@ -0,0 +1,20 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'timepicker', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_dep('third-party/android/support/v4:lib-support-v4'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':timepicker', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/DismissableTimePickerDialog.java b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/DismissableTimePickerDialog.java new file mode 100644 index 000000000..3baf4c924 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/DismissableTimePickerDialog.java @@ -0,0 +1,58 @@ +/** + * 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.timepicker; + +import javax.annotation.Nullable; + +import android.app.TimePickerDialog; +import android.content.Context; +import android.os.Build; + +/** + * + * Certain versions of Android (Jellybean-KitKat) have a bug where when dismissed, the + * {@link TimePickerDialog} still calls the OnTimeSetListener. This class works around that issue + * by *not* calling super.onStop on KitKat on lower, as that would erroneously call the + * OnTimeSetListener when the dialog is dismissed, or call it twice when "OK" is pressed. + * + * + * + * See: Issue 34833 + * + */ +public class DismissableTimePickerDialog extends TimePickerDialog { + + public DismissableTimePickerDialog( + Context context, + @Nullable OnTimeSetListener callback, + int hourOfDay, + int minute, + boolean is24HourView) { + super(context, callback, hourOfDay, minute, is24HourView); + } + + public DismissableTimePickerDialog( + Context context, + int theme, + @Nullable OnTimeSetListener callback, + int hourOfDay, + int minute, + boolean is24HourView) { + super(context, theme, callback, hourOfDay, minute, is24HourView); + } + + @Override + protected void onStop() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + super.onStop(); + } + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/SupportTimePickerDialogFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/SupportTimePickerDialogFragment.java new file mode 100644 index 000000000..d745984a1 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/SupportTimePickerDialogFragment.java @@ -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.timepicker; + +import javax.annotation.Nullable; + +import android.app.Dialog; +import android.app.TimePickerDialog.OnTimeSetListener; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; + +@SuppressWarnings("ValidFragment") +public class SupportTimePickerDialogFragment extends DialogFragment { + + @Nullable + private OnTimeSetListener mOnTimeSetListener; + @Nullable + private OnDismissListener mOnDismissListener; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle args = getArguments(); + return TimePickerDialogFragment.createDialog(args, getActivity(), mOnTimeSetListener); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (mOnDismissListener != null) { + mOnDismissListener.onDismiss(dialog); + } + } + + public void setOnDismissListener(@Nullable OnDismissListener onDismissListener) { + mOnDismissListener = onDismissListener; + } + + public void setOnTimeSetListener(@Nullable OnTimeSetListener onTimeSetListener) { + mOnTimeSetListener = onTimeSetListener; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/TimePickerDialogFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/TimePickerDialogFragment.java new file mode 100644 index 000000000..64005ae1a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/TimePickerDialogFragment.java @@ -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.timepicker; + +import javax.annotation.Nullable; + +import java.util.Calendar; + +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.TimePickerDialog.OnTimeSetListener; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.text.format.DateFormat; + +@SuppressWarnings("ValidFragment") +public class TimePickerDialogFragment extends DialogFragment { + + @Nullable + private OnTimeSetListener mOnTimeSetListener; + @Nullable + private OnDismissListener mOnDismissListener; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle args = getArguments(); + return createDialog(args, getActivity(), mOnTimeSetListener); + } + + /*package*/ static Dialog createDialog( + Bundle args, Context activityContext, @Nullable OnTimeSetListener onTimeSetListener + ) { + final Calendar now = Calendar.getInstance(); + int hour = now.get(Calendar.HOUR_OF_DAY); + int minute = now.get(Calendar.MINUTE); + boolean is24hour = DateFormat.is24HourFormat(activityContext); + + if (args != null) { + hour = args.getInt(TimePickerDialogModule.ARG_HOUR, now.get(Calendar.HOUR_OF_DAY)); + minute = args.getInt(TimePickerDialogModule.ARG_MINUTE, now.get(Calendar.MINUTE)); + is24hour = args.getBoolean( + TimePickerDialogModule.ARG_IS24HOUR, + DateFormat.is24HourFormat(activityContext)); + } + + return new DismissableTimePickerDialog( + activityContext, + onTimeSetListener, + hour, + minute, + is24hour); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (mOnDismissListener != null) { + mOnDismissListener.onDismiss(dialog); + } + } + + public void setOnDismissListener(@Nullable OnDismissListener onDismissListener) { + mOnDismissListener = onDismissListener; + } + + public void setOnTimeSetListener(@Nullable OnTimeSetListener onTimeSetListener) { + mOnTimeSetListener = onTimeSetListener; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/TimePickerDialogModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/TimePickerDialogModule.java new file mode 100644 index 000000000..7d627b583 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/timepicker/TimePickerDialogModule.java @@ -0,0 +1,153 @@ +/** + * 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.timepicker; + +import javax.annotation.Nullable; + +import java.util.Map; + +import android.app.Activity; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.app.TimePickerDialog.OnTimeSetListener; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.widget.TimePicker; + +import com.facebook.react.bridge.NativeModule; +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.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.annotations.VisibleForTesting; + +/** + * {@link NativeModule} that allows JS to show a native time picker dialog and get called back when + * the user selects a time. + */ +public class TimePickerDialogModule extends ReactContextBaseJavaModule { + + @VisibleForTesting + public static final String FRAGMENT_TAG = "TimePickerAndroid"; + + private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY"; + + /* package */ static final String ARG_HOUR = "hour"; + /* package */ static final String ARG_MINUTE = "minute"; + /* package */ static final String ARG_IS24HOUR = "is24Hour"; + /* package */ static final String ACTION_TIME_SET = "timeSetAction"; + /* package */ static final String ACTION_DISMISSED = "dismissedAction"; + + public TimePickerDialogModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "TimePickerAndroid"; + } + + private class TimePickerDialogListener implements OnTimeSetListener, OnDismissListener { + + private final Promise mPromise; + private boolean mPromiseResolved = false; + + public TimePickerDialogListener(Promise promise) { + mPromise = promise; + } + + @Override + public void onTimeSet(TimePicker view, int hour, int minute) { + if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) { + WritableMap result = new WritableNativeMap(); + result.putString("action", ACTION_TIME_SET); + result.putInt("hour", hour); + result.putInt("minute", minute); + mPromise.resolve(result); + mPromiseResolved = true; + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) { + WritableMap result = new WritableNativeMap(); + result.putString("action", ACTION_DISMISSED); + mPromise.resolve(result); + mPromiseResolved = true; + } + } + } + + @ReactMethod + public void open(@Nullable final ReadableMap options, Promise promise) { + + Activity activity = getCurrentActivity(); + if (activity == null) { + promise.reject( + ERROR_NO_ACTIVITY, + "Tried to open a TimePicker dialog while not attached to an Activity"); + return; + } + // We want to support both android.app.Activity and the pre-Honeycomb FragmentActivity + // (for apps that use it for legacy reasons). This unfortunately leads to some code duplication. + if (activity instanceof android.support.v4.app.FragmentActivity) { + android.support.v4.app.FragmentManager fragmentManager = + ((android.support.v4.app.FragmentActivity) activity).getSupportFragmentManager(); + android.support.v4.app.DialogFragment oldFragment = + (android.support.v4.app.DialogFragment)fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (oldFragment != null) { + oldFragment.dismiss(); + } + SupportTimePickerDialogFragment fragment = new SupportTimePickerDialogFragment(); + if (options != null) { + Bundle args = createFragmentArguments(options); + fragment.setArguments(args); + } + TimePickerDialogListener listener = new TimePickerDialogListener(promise); + fragment.setOnDismissListener(listener); + fragment.setOnTimeSetListener(listener); + fragment.show(fragmentManager, FRAGMENT_TAG); + } else { + FragmentManager fragmentManager = activity.getFragmentManager(); + DialogFragment oldFragment = (DialogFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (oldFragment != null) { + oldFragment.dismiss(); + } + TimePickerDialogFragment fragment = new TimePickerDialogFragment(); + if (options != null) { + final Bundle args = createFragmentArguments(options); + fragment.setArguments(args); + } + TimePickerDialogListener listener = new TimePickerDialogListener(promise); + fragment.setOnDismissListener(listener); + fragment.setOnTimeSetListener(listener); + fragment.show(fragmentManager, FRAGMENT_TAG); + } + } + + private Bundle createFragmentArguments(ReadableMap options) { + final Bundle args = new Bundle(); + if (options.hasKey(ARG_HOUR) && !options.isNull(ARG_HOUR)) { + args.putInt(ARG_HOUR, options.getInt(ARG_HOUR)); + } + if (options.hasKey(ARG_MINUTE) && !options.isNull(ARG_MINUTE)) { + args.putInt(ARG_MINUTE, options.getInt(ARG_MINUTE)); + } + if (options.hasKey(ARG_IS24HOUR) && !options.isNull(ARG_IS24HOUR)) { + args.putBoolean(ARG_IS24HOUR, options.getBoolean(ARG_IS24HOUR)); + } + return args; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index 193749ddf..8af3f1421 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -28,6 +28,7 @@ android_library( react_native_target('java/com/facebook/react/modules/camera:camera'), react_native_target('java/com/facebook/react/modules/clipboard:clipboard'), react_native_target('java/com/facebook/react/modules/core:core'), + react_native_target('java/com/facebook/react/modules/datepicker:datepicker'), react_native_target('java/com/facebook/react/modules/debug:debug'), react_native_target('java/com/facebook/react/modules/dialog:dialog'), react_native_target('java/com/facebook/react/modules/fresco:fresco'), @@ -36,6 +37,7 @@ android_library( react_native_target('java/com/facebook/react/modules/netinfo:netinfo'), react_native_target('java/com/facebook/react/modules/network:network'), react_native_target('java/com/facebook/react/modules/storage:storage'), + react_native_target('java/com/facebook/react/modules/timepicker:timepicker'), react_native_target('java/com/facebook/react/modules/toast:toast'), react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/modules/websocket:websocket'), 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 fa3e97e10..b817f55ac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -17,16 +17,19 @@ 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.appstate.AppStateModule; import com.facebook.react.modules.camera.CameraRollManager; +import com.facebook.react.modules.clipboard.ClipboardModule; import com.facebook.react.modules.dialog.DialogModule; +import com.facebook.react.modules.datepicker.DatePickerDialogModule; import com.facebook.react.modules.fresco.FrescoModule; import com.facebook.react.modules.intent.IntentModule; import com.facebook.react.modules.location.LocationModule; import com.facebook.react.modules.netinfo.NetInfoModule; import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.storage.AsyncStorageModule; +import com.facebook.react.modules.timepicker.TimePickerDialogModule; 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; @@ -50,7 +53,6 @@ import com.facebook.react.views.view.ReactViewManager; import com.facebook.react.views.viewpager.ReactViewPagerManager; import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager; import com.facebook.react.views.webview.ReactWebViewManager; -import com.facebook.react.modules.clipboard.ClipboardModule; /** * Package defining basic modules and view managers. @@ -64,12 +66,14 @@ public class MainReactPackage implements ReactPackage { new AsyncStorageModule(reactContext), new CameraRollManager(reactContext), new ClipboardModule(reactContext), + new DatePickerDialogModule(reactContext), new DialogModule(reactContext), new FrescoModule(reactContext), new IntentModule(reactContext), new LocationModule(reactContext), new NetworkingModule(reactContext), new NetInfoModule(reactContext), + new TimePickerDialogModule(reactContext), new ToastModule(reactContext), new WebSocketModule(reactContext)); }
+ * Certain versions of Android (Jellybean-KitKat) have a bug where when dismissed, the + * {@link DatePickerDialog} still calls the OnDateSetListener. This class works around that issue. + *
+ * See: Issue 34833 + *
+ * Certain versions of Android (Jellybean-KitKat) have a bug where when dismissed, the + * {@link TimePickerDialog} still calls the OnTimeSetListener. This class works around that issue + * by *not* calling super.onStop on KitKat on lower, as that would erroneously call the + * OnTimeSetListener when the dialog is dismissed, or call it twice when "OK" is pressed. + *