diff --git a/StatusPoC/App.js b/StatusPoC/App.js index b7d167c..0002c23 100644 --- a/StatusPoC/App.js +++ b/StatusPoC/App.js @@ -11,6 +11,7 @@ import { Platform, StyleSheet, Button, Text, TextInput, View } from 'react-nativ import ToastExample from './ToastExample' import CustomDialog from './CustomDialog' import SecureTextInput from './SecureTextInput' +import SecureTextInput2 from './SecureTextInput2' const instructions = Platform.select({ ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu', @@ -45,6 +46,8 @@ export default class App extends Component { {/* this.onChangeTextInput(text)} /> */} {this.state.text} + + ); } diff --git a/StatusPoC/SecureTextInput2.js b/StatusPoC/SecureTextInput2.js new file mode 100644 index 0000000..7ff8fcf --- /dev/null +++ b/StatusPoC/SecureTextInput2.js @@ -0,0 +1,932 @@ +// ImageView.js + +import {requireNativeComponent} from 'react-native'; + +/** + * Composes `View`. + * + * - src: string + * - borderRadius: number + * - resizeMode: 'cover' | 'contain' | 'stretch' + */ +//module.exports = requireNativeComponent('RCTSecureTextInput2'); +const EventEmitter = require('EventEmitter'); +const NativeMethodsMixin = require('NativeMethodsMixin'); +const Platform = require('Platform'); +const PropTypes = require('prop-types'); +const React = require('React'); +const ReactNative = require('ReactNative'); +const Text = require('Text'); +const TextAncestor = require('TextAncestor'); +const TextInputState = require('TextInputState'); +const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); +const UIManager = require('UIManager'); + +const createReactClass = require('create-react-class'); +const invariant = require('invariant'); + +const DataDetectorTypes = [ + 'phoneNumber', + 'link', + 'address', + 'calendarEvent', + 'none', + 'all', + ]; + +let AndroidTextInput; +if (Platform.OS === 'android') { + AndroidTextInput = requireNativeComponent('RCTSecureTextInput2'); +} + +const TextInput = createReactClass({ + displayName: 'SecureTextInput2', + statics: { + State: { + currentlyFocusedField: TextInputState.currentlyFocusedField, + focusTextInput: TextInputState.focusTextInput, + blurTextInput: TextInputState.blurTextInput, + }, + }, + propTypes: { + /** + * Can tell `TextInput` to automatically capitalize certain characters. + * + * - `characters`: all characters. + * - `words`: first letter of each word. + * - `sentences`: first letter of each sentence (*default*). + * - `none`: don't auto capitalize anything. + */ + autoCapitalize: PropTypes.oneOf([ + 'none', + 'sentences', + 'words', + 'characters', + ]), + /** + * If `false`, disables auto-correct. The default value is `true`. + */ + autoCorrect: PropTypes.bool, + /** + * If `false`, disables spell-check style (i.e. red underlines). + * The default value is inherited from `autoCorrect`. + * @platform ios + */ + spellCheck: PropTypes.bool, + /** + * If `true`, focuses the input on `componentDidMount`. + * The default value is `false`. + */ + autoFocus: PropTypes.bool, + /** + * Specifies whether fonts should scale to respect Text Size accessibility settings. The + * default is `true`. + */ + allowFontScaling: PropTypes.bool, + /** + * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. + * Possible values: + * `null/undefined` (default): inherit from the parent node or the global default (0) + * `0`: no max, ignore parent/global default + * `>= 1`: sets the maxFontSizeMultiplier of this node to this value + */ + maxFontSizeMultiplier: PropTypes.number, + /** + * If `false`, text is not editable. The default value is `true`. + */ + editable: PropTypes.bool, + /** + * Determines which keyboard to open, e.g.`numeric`. + * + * The following values work across platforms: + * + * - `default` + * - `numeric` + * - `number-pad` + * - `decimal-pad` + * - `email-address` + * - `phone-pad` + * + * *iOS Only* + * + * The following values work on iOS only: + * + * - `ascii-capable` + * - `numbers-and-punctuation` + * - `url` + * - `name-phone-pad` + * - `twitter` + * - `web-search` + * + * *Android Only* + * + * The following values work on Android only: + * + * - `visible-password` + */ + keyboardType: PropTypes.oneOf([ + // Cross-platform + 'default', + 'email-address', + 'numeric', + 'phone-pad', + 'number-pad', + // iOS-only + 'ascii-capable', + 'numbers-and-punctuation', + 'url', + 'name-phone-pad', + 'decimal-pad', + 'twitter', + 'web-search', + // Android-only + 'visible-password', + ]), + /** + * Determines the color of the keyboard. + * @platform ios + */ + keyboardAppearance: PropTypes.oneOf(['default', 'light', 'dark']), + /** + * Determines how the return key should look. On Android you can also use + * `returnKeyLabel`. + * + * *Cross platform* + * + * The following values work across platforms: + * + * - `done` + * - `go` + * - `next` + * - `search` + * - `send` + * + * *Android Only* + * + * The following values work on Android only: + * + * - `none` + * - `previous` + * + * *iOS Only* + * + * The following values work on iOS only: + * + * - `default` + * - `emergency-call` + * - `google` + * - `join` + * - `route` + * - `yahoo` + */ + returnKeyType: PropTypes.oneOf([ + // Cross-platform + 'done', + 'go', + 'next', + 'search', + 'send', + // Android-only + 'none', + 'previous', + // iOS-only + 'default', + 'emergency-call', + 'google', + 'join', + 'route', + 'yahoo', + ]), + /** + * Sets the return key to the label. Use it instead of `returnKeyType`. + * @platform android + */ + returnKeyLabel: PropTypes.string, + /** + * Limits the maximum number of characters that can be entered. Use this + * instead of implementing the logic in JS to avoid flicker. + */ + maxLength: PropTypes.number, + /** + * Sets the number of lines for a `TextInput`. Use it with multiline set to + * `true` to be able to fill the lines. + * @platform android + */ + numberOfLines: PropTypes.number, + /** + * When `false`, if there is a small amount of space available around a text input + * (e.g. landscape orientation on a phone), the OS may choose to have the user edit + * the text inside of a full screen text input mode. When `true`, this feature is + * disabled and users will always edit the text directly inside of the text input. + * Defaults to `false`. + * @platform android + */ + disableFullscreenUI: PropTypes.bool, + /** + * If `true`, the keyboard disables the return key when there is no text and + * automatically enables it when there is text. The default value is `false`. + * @platform ios + */ + enablesReturnKeyAutomatically: PropTypes.bool, + /** + * If `true`, the text input can be multiple lines. + * The default value is `false`. + */ + multiline: PropTypes.bool, + /** + * Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced` + * The default value is `simple`. + * @platform android + */ + textBreakStrategy: PropTypes.oneOf(['simple', 'highQuality', 'balanced']), + /** + * Callback that is called when the text input is blurred. + */ + onBlur: PropTypes.func, + /** + * Callback that is called when the text input is focused. + */ + onFocus: PropTypes.func, + /** + * Callback that is called when the text input's text changes. + */ + // onChange: PropTypes.func, + /** + * Callback that is called when the text input's text changes. + * Changed text is passed as an argument to the callback handler. + */ + // onChangeText: PropTypes.func, + /** + * Callback that is called when the text input's content size changes. + * This will be called with + * `{ nativeEvent: { contentSize: { width, height } } }`. + * + * Only called for multiline text inputs. + */ + onContentSizeChange: PropTypes.func, + /** + * Callback that is called when text input ends. + */ + onEndEditing: PropTypes.func, + /** + * Callback that is called when the text input selection is changed. + * This will be called with + * `{ nativeEvent: { selection: { start, end } } }`. + */ + onSelectionChange: PropTypes.func, + /** + * Callback that is called when the text input's submit button is pressed. + * Invalid if `multiline={true}` is specified. + */ + onSubmitEditing: PropTypes.func, + /** + * Callback that is called when a key is pressed. + * This will be called with `{ nativeEvent: { key: keyValue } }` + * where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and + * the typed-in character otherwise including `' '` for space. + * Fires before `onChange` callbacks. + */ + onKeyPress: PropTypes.func, + /** + * Invoked on mount and layout changes with `{x, y, width, height}`. + */ + onLayout: PropTypes.func, + /** + * Invoked on content scroll with `{ nativeEvent: { contentOffset: { x, y } } }`. + * May also contain other properties from ScrollEvent but on Android contentSize + * is not provided for performance reasons. + */ + onScroll: PropTypes.func, + /** + * The string that will be rendered before text input has been entered. + */ + placeholder: PropTypes.string, + /** + * The text color of the placeholder string. + */ + // placeholderTextColor: DeprecatedColorPropType, + /** + * If `false`, scrolling of the text view will be disabled. + * The default value is `true`. Does only work with 'multiline={true}'. + * @platform ios + */ + scrollEnabled: PropTypes.bool, + /** + * If `true`, the text input obscures the text entered so that sensitive text + * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. + */ + secureTextEntry: PropTypes.bool, + /** + * The highlight and cursor color of the text input. + */ + // selectionColor: DeprecatedColorPropType, + /** + * An instance of `DocumentSelectionState`, this is some state that is responsible for + * maintaining selection information for a document. + * + * Some functionality that can be performed with this instance is: + * + * - `blur()` + * - `focus()` + * - `update()` + * + * > You can reference `DocumentSelectionState` in + * > [`vendor/document/selection/DocumentSelectionState.js`](https://github.com/facebook/react-native/blob/master/Libraries/vendor/document/selection/DocumentSelectionState.js) + * + * @platform ios + */ + // selectionState: PropTypes.instanceOf(DocumentSelectionState), + /** + * The start and end of the text input's selection. Set start and end to + * the same value to position the cursor. + */ + selection: PropTypes.shape({ + start: PropTypes.number.isRequired, + end: PropTypes.number, + }), + /** + * The value to show for the text input. `TextInput` is a controlled + * component, which means the native value will be forced to match this + * value prop if provided. For most uses, this works great, but in some + * cases this may cause flickering - one common cause is preventing edits + * by keeping value the same. In addition to simply setting the same value, + * either set `editable={false}`, or set/update `maxLength` to prevent + * unwanted edits without flicker. + */ + value: PropTypes.string, + /** + * Provides an initial value that will change when the user starts typing. + * Useful for simple use-cases where you do not want to deal with listening + * to events and updating the value prop to keep the controlled state in sync. + */ + defaultValue: PropTypes.string, + /** + * When the clear button should appear on the right side of the text view. + * This property is supported only for single-line TextInput component. + * @platform ios + */ + clearButtonMode: PropTypes.oneOf([ + 'never', + 'while-editing', + 'unless-editing', + 'always', + ]), + /** + * If `true`, clears the text field automatically when editing begins. + * @platform ios + */ + clearTextOnFocus: PropTypes.bool, + /** + * If `true`, all text will automatically be selected on focus. + */ + selectTextOnFocus: PropTypes.bool, + /** + * If `true`, the text field will blur when submitted. + * The default value is true for single-line fields and false for + * multiline fields. Note that for multiline fields, setting `blurOnSubmit` + * to `true` means that pressing return will blur the field and trigger the + * `onSubmitEditing` event instead of inserting a newline into the field. + */ + blurOnSubmit: PropTypes.bool, + /** + * Note that not all Text styles are supported, an incomplete list of what is not supported includes: + * + * - `borderLeftWidth` + * - `borderTopWidth` + * - `borderRightWidth` + * - `borderBottomWidth` + * - `borderTopLeftRadius` + * - `borderTopRightRadius` + * - `borderBottomRightRadius` + * - `borderBottomLeftRadius` + * + * see [Issue#7070](https://github.com/facebook/react-native/issues/7070) + * for more detail. + * + * [Styles](docs/style.html) + */ + style: Text.propTypes.style, + /** + * The color of the `TextInput` underline. + * @platform android + */ + // underlineColorAndroid: DeprecatedColorPropType, + + /** + * If defined, the provided image resource will be rendered on the left. + * The image resource must be inside `/android/app/src/main/res/drawable` and referenced + * like + * ``` + * + * ``` + * @platform android + */ + inlineImageLeft: PropTypes.string, + + /** + * Padding between the inline image, if any, and the text input itself. + * @platform android + */ + inlineImagePadding: PropTypes.number, + + /** + * Determines the types of data converted to clickable URLs in the text input. + * Only valid if `multiline={true}` and `editable={false}`. + * By default no data types are detected. + * + * You can provide one type or an array of many types. + * + * Possible values for `dataDetectorTypes` are: + * + * - `'phoneNumber'` + * - `'link'` + * - `'address'` + * - `'calendarEvent'` + * - `'none'` + * - `'all'` + * + * @platform ios + */ + dataDetectorTypes: PropTypes.oneOfType([ + PropTypes.oneOf(DataDetectorTypes), + PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)), + ]), + /** + * If `true`, caret is hidden. The default value is `false`. + * This property is supported only for single-line TextInput component on iOS. + */ + caretHidden: PropTypes.bool, + /* + * If `true`, contextMenuHidden is hidden. The default value is `false`. + */ + contextMenuHidden: PropTypes.bool, + /** + * An optional identifier which links a custom InputAccessoryView to + * this text input. The InputAccessoryView is rendered above the + * keyboard when this text input is focused. + * @platform ios + */ + inputAccessoryViewID: PropTypes.string, + /** + * Give the keyboard and the system information about the + * expected semantic meaning for the content that users enter. + * @platform ios + */ + textContentType: PropTypes.oneOf([ + 'none', + 'URL', + 'addressCity', + 'addressCityAndState', + 'addressState', + 'countryName', + 'creditCardNumber', + 'emailAddress', + 'familyName', + 'fullStreetAddress', + 'givenName', + 'jobTitle', + 'location', + 'middleName', + 'name', + 'namePrefix', + 'nameSuffix', + 'nickname', + 'organizationName', + 'postalCode', + 'streetAddressLine1', + 'streetAddressLine2', + 'sublocality', + 'telephoneNumber', + 'username', + 'password', + 'newPassword', + 'oneTimeCode', + ]), + }, + getDefaultProps() { + return { + allowFontScaling: true, + underlineColorAndroid: 'transparent', + }; + }, + /** + * `NativeMethodsMixin` will look for this when invoking `setNativeProps`. We + * make `this` look like an actual native component class. + */ + mixins: [NativeMethodsMixin], + + /** + * Returns `true` if the input is currently focused; `false` otherwise. + */ + isFocused: function(): boolean { + return ( + TextInputState.currentlyFocusedField() === + ReactNative.findNodeHandle(this._inputRef) + ); + }, + + _inputRef: (undefined: any), + _focusSubscription: (undefined: ?Function), + // _lastNativeText: (undefined: ?string), + _lastNativeSelection: (undefined: ?Selection), + _rafId: (null: ?AnimationFrameID), + + componentDidMount: function() { + // this._lastNativeText = this.props.value; + const tag = ReactNative.findNodeHandle(this._inputRef); + if (tag != null) { + // tag is null only in unit tests + TextInputState.registerInput(tag); + } + + if (this.context.focusEmitter) { + this._focusSubscription = this.context.focusEmitter.addListener( + 'focus', + el => { + if (this === el) { + this._rafId = requestAnimationFrame(this.focus); + } else if (this.isFocused()) { + this.blur(); + } + }, + ); + if (this.props.autoFocus) { + this.context.onFocusRequested(this); + } + } else { + if (this.props.autoFocus) { + this._rafId = requestAnimationFrame(this.focus); + } + } + }, + + componentWillUnmount: function() { + this._focusSubscription && this._focusSubscription.remove(); + if (this.isFocused()) { + this.blur(); + } + const tag = ReactNative.findNodeHandle(this._inputRef); + if (tag != null) { + TextInputState.unregisterInput(tag); + } + if (this._rafId != null) { + cancelAnimationFrame(this._rafId); + } + }, + + contextTypes: { + onFocusRequested: PropTypes.func, + focusEmitter: PropTypes.instanceOf(EventEmitter), + }, + + /** + * Removes all text from the `TextInput`. + */ + clear: function() { + this.setNativeProps({text: ''}); + }, + + render: function() { + let textInput; + if (Platform.OS === 'ios') { + textInput = UIManager.getViewManagerConfig('RCTVirtualText') + ? this._renderIOS() + : this._renderIOSLegacy(); + } else if (Platform.OS === 'android') { + textInput = this._renderAndroid(); + } + return ( + {textInput} + ); + }, + + _getText: function(): ?string { + return typeof this.props.value === 'string' + ? this.props.value + : typeof this.props.defaultValue === 'string' + ? this.props.defaultValue + : ''; + }, + + _setNativeRef: function(ref: any) { + this._inputRef = ref; + }, + + _renderIOSLegacy: function() { + let textContainer; + + const props = Object.assign({}, this.props); + props.style = [this.props.style]; + + if (props.selection && props.selection.end == null) { + props.selection = { + start: props.selection.start, + end: props.selection.start, + }; + } + + if (!props.multiline) { + if (__DEV__) { + for (const propKey in onlyMultiline) { + if (props[propKey]) { + const error = new Error( + 'TextInput prop `' + + propKey + + '` is only supported with multiline.', + ); + warning(false, '%s', error.stack); + } + } + } + textContainer = ( + + ); + } else { + let children = props.children; + let childCount = 0; + React.Children.forEach(children, () => ++childCount); + invariant( + !(props.value && childCount), + 'Cannot specify both value and children.', + ); + if (childCount >= 1) { + children = ( + + {children} + + ); + } + if (props.inputView) { + children = [children, props.inputView]; + } + props.style.unshift(styles.multilineInput); + textContainer = ( + + ); + } + + return ( + + {textContainer} + + ); + }, + + _renderIOS: function() { + const props = Object.assign({}, this.props); + props.style = [this.props.style]; + + if (props.selection && props.selection.end == null) { + props.selection = { + start: props.selection.start, + end: props.selection.start, + }; + } + + const RCTTextInputView = props.multiline + ? RCTMultilineTextInputView + : RCTSinglelineTextInputView; + + if (props.multiline) { + props.style.unshift(styles.multilineInput); + } + + const textContainer = ( + + ); + + return ( + + {textContainer} + + ); + }, + + _renderAndroid: function() { + const props = Object.assign({}, this.props); + props.style = [this.props.style]; + // props.autoCapitalize = UIManager.getViewManagerConfig( + // 'AndroidTextInput', + // ).Constants.AutoCapitalizationType[props.autoCapitalize || 'sentences']; + // props.autoCapitalize = false; + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + let children = this.props.children; + let childCount = 0; + React.Children.forEach(children, () => ++childCount); + invariant( + !(this.props.value && childCount), + 'Cannot specify both value and children.', + ); + if (childCount > 1) { + children = {children}; + } + + if (props.selection && props.selection.end == null) { + props.selection = { + start: props.selection.start, + end: props.selection.start, + }; + } + + const textContainer = ( + + ); + + return ( + + {textContainer} + + ); + }, + + _onFocus: function(event: FocusEvent) { + if (this.props.onFocus) { + this.props.onFocus(event); + } + + if (this.props.selectionState) { + this.props.selectionState.focus(); + } + }, + + _onPress: function(event: PressEvent) { + if (this.props.editable || this.props.editable === undefined) { + this.focus(); + } + }, + + _onChange: function(event: ChangeEvent) { + // Make sure to fire the mostRecentEventCount first so it is already set on + // native when the text value is set. + if (this._inputRef && this._inputRef.setNativeProps) { + this._inputRef.setNativeProps({ + mostRecentEventCount: event.nativeEvent.eventCount, + }); + } + + // const text = event.nativeEvent.text; + // this.props.onChange && this.props.onChange(event); + // this.props.onChangeText && this.props.onChangeText(text); + + if (!this._inputRef) { + // calling `this.props.onChange` or `this.props.onChangeText` + // may clean up the input itself. Exits here. + return; + } + + // this._lastNativeText = text; + this.forceUpdate(); + }, + + _onSelectionChange: function(event: SelectionChangeEvent) { + this.props.onSelectionChange && this.props.onSelectionChange(event); + + if (!this._inputRef) { + // calling `this.props.onSelectionChange` + // may clean up the input itself. Exits here. + return; + } + + this._lastNativeSelection = event.nativeEvent.selection; + + if (this.props.selection || this.props.selectionState) { + this.forceUpdate(); + } + }, + + componentDidUpdate: function() { + // This is necessary in case native updates the text and JS decides + // that the update should be ignored and we should stick with the value + // that we have in JS. + const nativeProps = {}; + + // if ( + // this._lastNativeText !== this.props.value && + // typeof this.props.value === 'string' + // ) { + // nativeProps.text = this.props.value; + // } + + // Selection is also a controlled prop, if the native value doesn't match + // JS, update to the JS value. + const {selection} = this.props; + if ( + this._lastNativeSelection && + selection && + (this._lastNativeSelection.start !== selection.start || + this._lastNativeSelection.end !== selection.end) + ) { + nativeProps.selection = this.props.selection; + } + + if ( + Object.keys(nativeProps).length > 0 && + this._inputRef && + this._inputRef.setNativeProps + ) { + this._inputRef.setNativeProps(nativeProps); + } + + if (this.props.selectionState && selection) { + this.props.selectionState.update(selection.start, selection.end); + } + }, + + _onBlur: function(event: BlurEvent) { + // This is a hack to fix https://fburl.com/toehyir8 + // @todo(rsnara) Figure out why this is necessary. + this.blur(); + if (this.props.onBlur) { + this.props.onBlur(event); + } + + if (this.props.selectionState) { + this.props.selectionState.blur(); + } + }, + + _onScroll: function(event: ScrollEvent) { + this.props.onScroll && this.props.onScroll(event); + }, + }); + + module.exports = TextInput; diff --git a/StatusPoC/android/app/src/main/java/com/statuspoc/FireMissilesDialogFragment.java b/StatusPoC/android/app/src/main/java/com/statuspoc/FireMissilesDialogFragment.java index fa48650..9d6c10f 100644 --- a/StatusPoC/android/app/src/main/java/com/statuspoc/FireMissilesDialogFragment.java +++ b/StatusPoC/android/app/src/main/java/com/statuspoc/FireMissilesDialogFragment.java @@ -11,7 +11,8 @@ public class FireMissilesDialogFragment extends DialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { // Use the Builder class for convenient dialog construction AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - String text = ReactSecureTextInputManager.getText("XYZ"); + //String text = ReactSecureTextInputManager.getText("XYZ"); + String text = SecureTextInputManager.getText("XYZ"); builder.setMessage("Fire missiles?") .setPositiveButton(text, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { diff --git a/StatusPoC/android/app/src/main/java/com/statuspoc/MainApplication.java b/StatusPoC/android/app/src/main/java/com/statuspoc/MainApplication.java index f12b309..f1aa7a0 100644 --- a/StatusPoC/android/app/src/main/java/com/statuspoc/MainApplication.java +++ b/StatusPoC/android/app/src/main/java/com/statuspoc/MainApplication.java @@ -43,6 +43,7 @@ public class MainApplication extends Application implements ReactApplication { @Override public void onCreate() { ReactSecureTextInputManager.setText("XYZ", "Initial text"); + SecureTextInputManager.setText("XYZ", "Initial text"); super.onCreate(); SoLoader.init(this, /* native exopackage */ false); } diff --git a/StatusPoC/android/app/src/main/java/com/statuspoc/ReactSecureTextInputPackage.java b/StatusPoC/android/app/src/main/java/com/statuspoc/ReactSecureTextInputPackage.java index 17f6208..55ff255 100644 --- a/StatusPoC/android/app/src/main/java/com/statuspoc/ReactSecureTextInputPackage.java +++ b/StatusPoC/android/app/src/main/java/com/statuspoc/ReactSecureTextInputPackage.java @@ -19,7 +19,8 @@ public class ReactSecureTextInputPackage implements ReactPackage { public List createViewManagers( ReactApplicationContext reactContext) { return Arrays.asList( - new ReactSecureTextInputManager() + new ReactSecureTextInputManager(), + new SecureTextInputManager() ); } diff --git a/StatusPoC/android/app/src/main/java/com/statuspoc/SecureEditText.java b/StatusPoC/android/app/src/main/java/com/statuspoc/SecureEditText.java new file mode 100644 index 0000000..f8c29d9 --- /dev/null +++ b/StatusPoC/android/app/src/main/java/com/statuspoc/SecureEditText.java @@ -0,0 +1,411 @@ +package com.statuspoc; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Build; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.KeyListener; +import android.text.method.QwertyKeyListener; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.views.textinput.ContentSizeWatcher; +import com.facebook.react.views.textinput.ReactTextInputLocalData; +import com.facebook.react.views.view.ReactViewBackgroundManager; +import javax.annotation.Nullable; + +/** + * A wrapper around the EditText that lets us better control what happens when an EditText gets + * focused or blurred, and when to display the soft keyboard and when not to. + * + * ReactEditTexts have setFocusableInTouchMode set to false automatically because touches on the + * EditText are managed on the JS side. This also removes the nasty side effect that EditTexts + * have, which is that focus is always maintained on one of the EditTexts. + */ +public class SecureEditText extends EditText { + private SecureTextInputManager mManager; + private final InputMethodManager mInputMethodManager; + // This component is controlled, so we want it to get focused only when JS ask it to do so. + // Whenever android requests focus (which it does for random reasons), it will be ignored. + private boolean mIsJSSettingFocus; + private int mStagedInputType; + private @Nullable Boolean mBlurOnSubmit; + private boolean mDisableFullscreen; + private @Nullable String mReturnKeyType; + private @Nullable ContentSizeWatcher mContentSizeWatcher; + private final InternalKeyListener mKeyListener; + private float mLetterSpacingPt = 0; + + private ReactViewBackgroundManager mReactBackgroundManager; + + private static final KeyListener sKeyListener = QwertyKeyListener.getInstanceForFullKeyboard(); + + public SecureEditText(SecureTextInputManager manager, Context context) { + super(context); + setFocusableInTouchMode(false); + + mManager = manager; + mReactBackgroundManager = new ReactViewBackgroundManager(this); + mInputMethodManager = (InputMethodManager) + Assertions.assertNotNull(getContext().getSystemService(Context.INPUT_METHOD_SERVICE)); + mIsJSSettingFocus = false; + mBlurOnSubmit = null; + mDisableFullscreen = false; + mStagedInputType = getInputType(); + mKeyListener = new InternalKeyListener(); + } + + @Override + public final void addTextChangedListener(TextWatcher watcher) throws java.lang.SecurityException { + if (!(watcher instanceof SecureTextInputManager.SecureReactTextInputTextWatcher)) { + // Explicitly disallow third-party subscribers + throw new java.lang.SecurityException(); + } + + super.addTextChangedListener(watcher); + } + + public void setContentSizeWatcher(ContentSizeWatcher contentSizeWatcher) { + mContentSizeWatcher = contentSizeWatcher; + } + + // After the text changes inside an EditText, TextView checks if a layout() has been requested. + // If it has, it will not scroll the text to the end of the new text inserted, but wait for the + // next layout() to be called. However, we do not perform a layout() after a requestLayout(), so + // we need to override isLayoutRequested to force EditText to scroll to the end of the new text + // immediately. + // TODO: t6408636 verify if we should schedule a layout after a View does a requestLayout() + @Override + public boolean isLayoutRequested() { + return false; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + onContentSizeChange(); + } + + // Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't + // since we only allow JS to change focus, which in turn causes TextView to crash. + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER && !isMultiline()) { + hideSoftKeyboard(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + ReactContext reactContext = (ReactContext) getContext(); + InputConnection inputConnection = super.onCreateInputConnection(outAttrs); + // if (inputConnection != null && mOnKeyPress) { + // inputConnection = new ReactEditTextInputConnectionWrapper(inputConnection, reactContext, this); + // } + + if (isMultiline() && getBlurOnSubmit()) { + // Remove IME_FLAG_NO_ENTER_ACTION to keep the original IME_OPTION + outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; + } + return inputConnection; + } + + @Override + public void clearFocus() { + setFocusableInTouchMode(false); + super.clearFocus(); + hideSoftKeyboard(); + } + + @Override + public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + // Always return true if we are already focused. This is used by android in certain places, + // such as text selection. + if (isFocused()) { + return true; + } + if (!mIsJSSettingFocus) { + return false; + } + setFocusableInTouchMode(true); + boolean focused = super.requestFocus(direction, previouslyFocusedRect); + showSoftKeyboard(); + return focused; + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // if (!mIsSettingTextFromJS && mListeners != null) { + // for (TextWatcher listener : mListeners) { + // listener.onTextChanged(s, start, before, count); + // } + // } + + onContentSizeChange(); + } + + private boolean isMultiline() { + return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0; + } + + private boolean isSecureText() { + return + (getInputType() & + (InputType.TYPE_NUMBER_VARIATION_PASSWORD | + InputType.TYPE_TEXT_VARIATION_PASSWORD)) + != 0; + } + + private void onContentSizeChange() { + // if (mContentSizeWatcher != null) { + // mContentSizeWatcher.onLayout(); + // } + + setIntrinsicContentSize(); + } + + private void setIntrinsicContentSize() { + ReactContext reactContext = (ReactContext) getContext(); + UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class); + final ReactTextInputLocalData localData = new ReactTextInputLocalData(this); + uiManager.setViewLocalData(getId(), localData); + } + + private void updateImeOptions() { + // Default to IME_ACTION_DONE + int returnKeyFlag = EditorInfo.IME_ACTION_DONE; + if (mReturnKeyType != null) { + switch (mReturnKeyType) { + case "go": + returnKeyFlag = EditorInfo.IME_ACTION_GO; + break; + case "next": + returnKeyFlag = EditorInfo.IME_ACTION_NEXT; + break; + case "none": + returnKeyFlag = EditorInfo.IME_ACTION_NONE; + break; + case "previous": + returnKeyFlag = EditorInfo.IME_ACTION_PREVIOUS; + break; + case "search": + returnKeyFlag = EditorInfo.IME_ACTION_SEARCH; + break; + case "send": + returnKeyFlag = EditorInfo.IME_ACTION_SEND; + break; + case "done": + returnKeyFlag = EditorInfo.IME_ACTION_DONE; + break; + } + } + + if (mDisableFullscreen) { + setImeOptions(returnKeyFlag | EditorInfo.IME_FLAG_NO_FULLSCREEN); + } else { + setImeOptions(returnKeyFlag); + } + } + + public void setBlurOnSubmit(@Nullable Boolean blurOnSubmit) { + mBlurOnSubmit = blurOnSubmit; + } + + public boolean getBlurOnSubmit() { + if (mBlurOnSubmit == null) { + // Default blurOnSubmit + return isMultiline() ? false : true; + } + + return mBlurOnSubmit; + } + + public void setDisableFullscreenUI(boolean disableFullscreenUI) { + mDisableFullscreen = disableFullscreenUI; + updateImeOptions(); + } + + public boolean getDisableFullscreenUI() { + return mDisableFullscreen; + } + + public void setReturnKeyType(String returnKeyType) { + mReturnKeyType = returnKeyType; + updateImeOptions(); + } + + public String getReturnKeyType() { + return mReturnKeyType; + } + + /*protected*/ int getStagedInputType() { + return mStagedInputType; + } + + /*package*/ void setStagedInputType(int stagedInputType) { + mStagedInputType = stagedInputType; + } + + /*package*/ void commitStagedInputType() { + if (getInputType() != mStagedInputType) { + int selectionStart = getSelectionStart(); + int selectionEnd = getSelectionEnd(); + setInputType(mStagedInputType); + setSelection(selectionStart, selectionEnd); + } + } + + @Override + public void setInputType(int type) { + Typeface tf = super.getTypeface(); + super.setInputType(type); + mStagedInputType = type; + // Input type password defaults to monospace font, so we need to re-apply the font + super.setTypeface(tf); + + // We override the KeyListener so that all keys on the soft input keyboard as well as hardware + // keyboards work. Some KeyListeners like DigitsKeyListener will display the keyboard but not + // accept all input from it + mKeyListener.setInputType(type); + setKeyListener(mKeyListener); + } + + // VisibleForTesting from {@link TextInputEventsTestCase}. + public void requestFocusFromJS() { + mIsJSSettingFocus = true; + requestFocus(); + mIsJSSettingFocus = false; + } + + /* package */ void clearFocusFromJS() { + clearFocus(); + } + + private boolean showSoftKeyboard() { + return mInputMethodManager.showSoftInput(this, 0); + } + + private void hideSoftKeyboard() { + mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + this.mManager.onDetachedFromWindow(this); + } + + @Override + public void setBackgroundColor(int color) { + mReactBackgroundManager.setBackgroundColor(color); + } + + public void setBorderWidth(int position, float width) { + mReactBackgroundManager.setBorderWidth(position, width); + } + + public void setBorderColor(int position, float color, float alpha) { + mReactBackgroundManager.setBorderColor(position, color, alpha); + } + + public void setBorderRadius(float borderRadius) { + mReactBackgroundManager.setBorderRadius(borderRadius); + } + + public void setBorderRadius(float borderRadius, int position) { + mReactBackgroundManager.setBorderRadius(borderRadius, position); + } + + public void setBorderStyle(@Nullable String style) { + mReactBackgroundManager.setBorderStyle(style); + } + + public void setLetterSpacingPt(float letterSpacingPt) { + mLetterSpacingPt = letterSpacingPt; + updateLetterSpacing(); + } + + @Override + public void setTextSize (float size) { + super.setTextSize(size); + updateLetterSpacing(); + } + + @Override + public void setTextSize (int unit, float size) { + super.setTextSize(unit, size); + updateLetterSpacing(); + } + + protected void updateLetterSpacing() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setLetterSpacing(PixelUtil.toPixelFromSP(mLetterSpacingPt) / getTextSize()); + } + } + + /* + * This class is set as the KeyListener for the underlying TextView + * It does two things + * 1) Provides the same answer to getInputType() as the real KeyListener would have which allows + * the proper keyboard to pop up on screen + * 2) Permits all keyboard input through + */ + private static class InternalKeyListener implements KeyListener { + + private int mInputType = 0; + + public InternalKeyListener() { + } + + public void setInputType(int inputType) { + mInputType = inputType; + } + + /* + * getInputType will return whatever value is passed in. This will allow the proper keyboard + * to be shown on screen but without the actual filtering done by other KeyListeners + */ + @Override + public int getInputType() { + return mInputType; + } + + /* + * All overrides of key handling defer to the underlying KeyListener which is shared by all + * ReactEditText instances. It will basically allow any/all keyboard input whether from + * physical keyboard or from soft input. + */ + @Override + public boolean onKeyDown(View view, Editable text, int keyCode, KeyEvent event) { + return sKeyListener.onKeyDown(view, text, keyCode, event); + } + + @Override + public boolean onKeyUp(View view, Editable text, int keyCode, KeyEvent event) { + return sKeyListener.onKeyUp(view, text, keyCode, event); + } + + @Override + public boolean onKeyOther(View view, Editable text, KeyEvent event) { + return sKeyListener.onKeyOther(view, text, event); + } + + @Override + public void clearMetaKeyState(View view, Editable content, int states) { + sKeyListener.clearMetaKeyState(view, content, states); + } + } +} \ No newline at end of file diff --git a/StatusPoC/android/app/src/main/java/com/statuspoc/SecureTextInputManager.java b/StatusPoC/android/app/src/main/java/com/statuspoc/SecureTextInputManager.java new file mode 100644 index 0000000..ef68a01 --- /dev/null +++ b/StatusPoC/android/app/src/main/java/com/statuspoc/SecureTextInputManager.java @@ -0,0 +1,451 @@ +package com.statuspoc; + +import android.util.Log; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.TypedValue; +import android.widget.EditText; +import android.util.ArrayMap; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.BaseViewManager; +import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.Spacing; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewDefaults; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.text.ReactTextUpdate; +import com.facebook.react.views.textinput.ContentSizeWatcher; +import com.facebook.react.views.textinput.ReactTextInputShadowNode; +import com.facebook.react.views.textinput.ReactContentSizeChangedEvent; +import com.facebook.yoga.YogaConstants; +import java.util.Map; + +import javax.annotation.Nullable; + +/** + * Manages instances of SecureTextInput2. + */ +@ReactModule(name = SecureTextInputManager.REACT_CLASS) +public class SecureTextInputManager extends BaseViewManager { + + protected static final String REACT_CLASS = "RCTSecureTextInput2"; + private static final ArrayMap registrationMap = new ArrayMap(); + private static final ArrayMap viewToIdMap = new ArrayMap(); + private static final ArrayMap idToViewMap = new ArrayMap(); + + private static final int[] SPACING_TYPES = { + Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM, + }; + + private static final int FOCUS_TEXT_INPUT = 1; + private static final int BLUR_TEXT_INPUT = 2; + + private static final int INPUT_TYPE_KEYBOARD_NUMBER_PAD = InputType.TYPE_CLASS_NUMBER; + private static final int INPUT_TYPE_KEYBOARD_DECIMAL_PAD = INPUT_TYPE_KEYBOARD_NUMBER_PAD | + InputType.TYPE_NUMBER_FLAG_DECIMAL; + private static final int INPUT_TYPE_KEYBOARD_NUMBERED = INPUT_TYPE_KEYBOARD_DECIMAL_PAD | + InputType.TYPE_NUMBER_FLAG_SIGNED; + private static final int PASSWORD_VISIBILITY_FLAG = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD & + ~InputType.TYPE_TEXT_VARIATION_PASSWORD; + private static final int KEYBOARD_TYPE_FLAGS = INPUT_TYPE_KEYBOARD_NUMBERED | + InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | + InputType.TYPE_CLASS_TEXT | InputType.TYPE_CLASS_PHONE | + PASSWORD_VISIBILITY_FLAG; + + private static final String KEYBOARD_TYPE_EMAIL_ADDRESS = "email-address"; + private static final String KEYBOARD_TYPE_NUMERIC = "numeric"; + private static final String KEYBOARD_TYPE_DECIMAL_PAD = "decimal-pad"; + private static final String KEYBOARD_TYPE_NUMBER_PAD = "number-pad"; + private static final String KEYBOARD_TYPE_PHONE_PAD = "phone-pad"; + private static final String KEYBOARD_TYPE_VISIBLE_PASSWORD = "visible-password"; + // private static final InputFilter[] EMPTY_FILTERS = new InputFilter[0]; + private static final int UNSET = -1; + + @Override + public String getName() { + return REACT_CLASS; + } + + @ReactProp(name = "registrationID") + public void setRegistrationId(SecureEditText view, String id) throws java.lang.SecurityException, IllegalArgumentException { + if (view == null) { + throw new IllegalArgumentException("view"); + } + if (id == null) { + throw new IllegalArgumentException("id"); + } + + if (!registrationMap.containsKey(id)) { + registrationMap.put(id, view.getText().toString()); + } + viewToIdMap.put(view, id); + idToViewMap.put(id, view); + + view.addTextChangedListener(new SecureReactTextInputTextWatcher(view)); + // view.setOnKeyPress(false); + } + + public static String getText(final String id) { + return registrationMap.get(id); + } + + public static void setText(final String id, final String value) { + registrationMap.put(id, value); + + Log.d(REACT_CLASS, "setText called with " + value); + final EditText view = idToViewMap.get(id); + if (view != null) { + view.setText(value); + Log.d(REACT_CLASS, "called setText on EditText with " + value); + } + } + + @Override + public final SecureEditText createViewInstance(ThemedReactContext context) { + SecureEditText editText = new SecureEditText(this, context); + int inputType = editText.getInputType(); + editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE)); + // editText.setReturnKeyType("done"); + editText.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))); + return editText; + } + + @Override + public LayoutShadowNode createShadowNodeInstance() { + return new ReactTextInputShadowNode(); + } + + @Override + public Class getShadowNodeClass() { + return ReactTextInputShadowNode.class; + } + + void onDetachedFromWindow(SecureEditText view) { + final String id = this.viewToIdMap.get(view); + if (id == null) { + Log.d(REACT_CLASS, "unknown SecureEditText detached"); + return; + } + + this.viewToIdMap.remove(view); + this.idToViewMap.remove(id); + this.registrationMap.remove(id); + } + + class SecureReactTextInputTextWatcher implements TextWatcher { + + private SecureEditText mEditText; + private String mPreviousText; + + public SecureReactTextInputTextWatcher( + final SecureEditText editText) { + mEditText = editText; + mPreviousText = null; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Incoming charSequence gets mutated before onTextChanged() is invoked + mPreviousText = s.toString(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Rearranging the text (i.e. changing between singleline and multiline attributes) can + // also trigger onTextChanged + if (count == 0 && before == 0) { + return; + } + + Assertions.assertNotNull(mPreviousText); + String newText = s.toString().substring(start, start + count); + String oldText = mPreviousText.substring(start, start + before); + // Don't send same text changes + if (count == before && newText.equals(oldText)) { + return; + } + + newText = s.toString(); + //Log.d(REACT_CLASS, "newText: " + newText); + registrationMap.put(viewToIdMap.get(mEditText), newText); + } + + @Override + public void afterTextChanged(Editable s) { + } + } + + private class SecureReactContentSizeWatcher implements ContentSizeWatcher { + private SecureEditText mEditText; + private EventDispatcher mEventDispatcher; + private int mPreviousContentWidth = 0; + private int mPreviousContentHeight = 0; + + public SecureReactContentSizeWatcher(SecureEditText editText) { + mEditText = editText; + ReactContext reactContext = (ReactContext) editText.getContext(); + mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + } + + @Override + public void onLayout() { + int contentWidth = mEditText.getWidth(); + int contentHeight = mEditText.getHeight(); + + // Use instead size of text content within EditText when available + if (mEditText.getLayout() != null) { + contentWidth = mEditText.getCompoundPaddingLeft() + mEditText.getLayout().getWidth() + + mEditText.getCompoundPaddingRight(); + contentHeight = mEditText.getCompoundPaddingTop() + mEditText.getLayout().getHeight() + + mEditText.getCompoundPaddingBottom(); + } + + if (contentWidth != mPreviousContentWidth || contentHeight != mPreviousContentHeight) { + mPreviousContentHeight = contentHeight; + mPreviousContentWidth = contentWidth; + + mEventDispatcher.dispatchEvent( + new ReactContentSizeChangedEvent( + mEditText.getId(), + PixelUtil.toDIPFromPixel(contentWidth), + PixelUtil.toDIPFromPixel(contentHeight))); + } + } + } + + @Override + public @Nullable Map getCommandsMap() { + return MapBuilder.of("focusTextInput", FOCUS_TEXT_INPUT, "blurTextInput", BLUR_TEXT_INPUT); + } + + @Override + public void receiveCommand( + SecureEditText reactEditText, + int commandId, + @Nullable ReadableArray args) { + switch (commandId) { + case FOCUS_TEXT_INPUT: + reactEditText.requestFocusFromJS(); + break; + case BLUR_TEXT_INPUT: + reactEditText.clearFocusFromJS(); + break; + } + } + + @Override + public void updateExtraData(SecureEditText view, Object extraData) { + if (extraData instanceof ReactTextUpdate) { + ReactTextUpdate update = (ReactTextUpdate) extraData; + + view.setPadding( + (int) update.getPaddingLeft(), + (int) update.getPaddingTop(), + (int) update.getPaddingRight(), + (int) update.getPaddingBottom()); + + final String id = viewToIdMap.get(view); + final String text = getText(id); + view.setText(text); + Log.d(REACT_CLASS, "called setText on EditText from updateExtraData with " + text); + registrationMap.put(id, text); + } + } + + @ReactProp(name = "blurOnSubmit") + public void setBlurOnSubmit(SecureEditText view, @Nullable Boolean blurOnSubmit) { + view.setBlurOnSubmit(blurOnSubmit); + } + + @ReactProp(name = "onContentSizeChange", defaultBoolean = false) + public void setOnContentSizeChange(final SecureEditText view, boolean onContentSizeChange) { + if (onContentSizeChange) { + view.setContentSizeWatcher(new SecureReactContentSizeWatcher(view)); + } else { + view.setContentSizeWatcher(null); + } + } + + @ReactProp(name = "editable", defaultBoolean = true) + public void setEditable(SecureEditText view, boolean editable) { + view.setEnabled(editable); + } + + @ReactProp(name = "multiline", defaultBoolean = false) + public void setMultiline(SecureEditText view, boolean multiline) { + updateStagedInputTypeFlag( + view, + multiline ? 0 : InputType.TYPE_TEXT_FLAG_MULTI_LINE, + multiline ? InputType.TYPE_TEXT_FLAG_MULTI_LINE : 0); + } + + @ReactProp(name = "secureTextEntry", defaultBoolean = false) + public void setSecureTextEntry(SecureEditText view, boolean password) { + updateStagedInputTypeFlag( + view, + password ? 0 : + InputType.TYPE_NUMBER_VARIATION_PASSWORD | InputType.TYPE_TEXT_VARIATION_PASSWORD, + password ? InputType.TYPE_TEXT_VARIATION_PASSWORD : 0); + checkPasswordType(view); + } + + @ReactProp(name = "keyboardType") + public void setKeyboardType(SecureEditText view, @Nullable String keyboardType) { + int flagsToSet = InputType.TYPE_CLASS_TEXT; + if (KEYBOARD_TYPE_NUMERIC.equalsIgnoreCase(keyboardType)) { + flagsToSet = INPUT_TYPE_KEYBOARD_NUMBERED; + } else if (KEYBOARD_TYPE_NUMBER_PAD.equalsIgnoreCase(keyboardType)) { + flagsToSet = INPUT_TYPE_KEYBOARD_NUMBER_PAD; + } else if (KEYBOARD_TYPE_DECIMAL_PAD.equalsIgnoreCase(keyboardType)) { + flagsToSet = INPUT_TYPE_KEYBOARD_DECIMAL_PAD; + } else if (KEYBOARD_TYPE_EMAIL_ADDRESS.equalsIgnoreCase(keyboardType)) { + flagsToSet = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_CLASS_TEXT; + } else if (KEYBOARD_TYPE_PHONE_PAD.equalsIgnoreCase(keyboardType)) { + flagsToSet = InputType.TYPE_CLASS_PHONE; + } else if (KEYBOARD_TYPE_VISIBLE_PASSWORD.equalsIgnoreCase(keyboardType)) { + // This will supercede secureTextEntry={false}. If it doesn't, due to the way + // the flags work out, the underlying field will end up a URI-type field. + flagsToSet = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; + } + updateStagedInputTypeFlag( + view, + KEYBOARD_TYPE_FLAGS, + flagsToSet); + checkPasswordType(view); + } + + @ReactProp(name = "returnKeyType") + public void setReturnKeyType(SecureEditText view, String returnKeyType) { + view.setReturnKeyType(returnKeyType); + } + + @ReactProp(name = "disableFullscreenUI", defaultBoolean = false) + public void setDisableFullscreenUI(SecureEditText view, boolean disableFullscreenUI) { + view.setDisableFullscreenUI(disableFullscreenUI); + } + + private static final int IME_ACTION_ID = 0x670; + + @ReactProp(name = "returnKeyLabel") + public void setReturnKeyLabel(SecureEditText view, String returnKeyLabel) { + view.setImeActionLabel(returnKeyLabel, IME_ACTION_ID); + } + + @ReactPropGroup(names = { + ViewProps.BORDER_RADIUS, + ViewProps.BORDER_TOP_LEFT_RADIUS, + ViewProps.BORDER_TOP_RIGHT_RADIUS, + ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, + ViewProps.BORDER_BOTTOM_LEFT_RADIUS + }, defaultFloat = YogaConstants.UNDEFINED) + public void setBorderRadius(SecureEditText view, int index, float borderRadius) { + if (!YogaConstants.isUndefined(borderRadius)) { + borderRadius = PixelUtil.toPixelFromDIP(borderRadius); + } + + if (index == 0) { + view.setBorderRadius(borderRadius); + } else { + view.setBorderRadius(borderRadius, index - 1); + } + } + + @ReactProp(name = "borderStyle") + public void setBorderStyle(SecureEditText view, @Nullable String borderStyle) { + view.setBorderStyle(borderStyle); + } + + @ReactPropGroup(names = { + ViewProps.BORDER_WIDTH, + ViewProps.BORDER_LEFT_WIDTH, + ViewProps.BORDER_RIGHT_WIDTH, + ViewProps.BORDER_TOP_WIDTH, + ViewProps.BORDER_BOTTOM_WIDTH, + }, defaultFloat = YogaConstants.UNDEFINED) + public void setBorderWidth(SecureEditText view, int index, float width) { + if (!YogaConstants.isUndefined(width)) { + width = PixelUtil.toPixelFromDIP(width); + } + view.setBorderWidth(SPACING_TYPES[index], width); + } + + @ReactPropGroup(names = { + "borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor" + }, customType = "Color") + public void setBorderColor(SecureEditText view, int index, Integer color) { + float rgbComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int)color & 0x00FFFFFF); + float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int)color >>> 24); + view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent); + } + + @Override + protected void onAfterUpdateTransaction(SecureEditText view) { + super.onAfterUpdateTransaction(view); + view.commitStagedInputType(); + } + + // Sets the correct password type, since numeric and text passwords have different types + private static void checkPasswordType(SecureEditText view) { + if ((view.getStagedInputType() & INPUT_TYPE_KEYBOARD_NUMBERED) != 0 && + (view.getStagedInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD) != 0) { + // Text input type is numbered password, remove text password variation, add numeric one + updateStagedInputTypeFlag( + view, + InputType.TYPE_TEXT_VARIATION_PASSWORD, + InputType.TYPE_NUMBER_VARIATION_PASSWORD); + } + } + + private static void updateStagedInputTypeFlag( + SecureEditText view, + int flagsToUnset, + int flagsToSet) { + view.setStagedInputType((view.getStagedInputType() & ~flagsToUnset) | flagsToSet); + } + + @Override + protected final void addEventEmitters( + final ThemedReactContext reactContext, + final SecureEditText editText) { + // editText.setOnEditorActionListener( + // new TextView.OnEditorActionListener() { + // @Override + // public boolean onEditorAction(TextView v, int actionId, KeyEvent keyEvent) { + // // Any 'Enter' action will do + // if ((actionId & EditorInfo.IME_MASK_ACTION) > 0 || + // actionId == EditorInfo.IME_NULL) { + // boolean blurOnSubmit = editText.getBlurOnSubmit(); + // boolean isMultiline = ((editText.getInputType() & + // InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0); + + // // Motivation: + // // * blurOnSubmit && isMultiline => Clear focus; prevent default behaviour (return true); + // // * blurOnSubmit && !isMultiline => Clear focus; prevent default behaviour (return true); + // // * !blurOnSubmit && isMultiline => Perform default behaviour (return false); + // // * !blurOnSubmit && !isMultiline => Prevent default behaviour (return true). + + // if (blurOnSubmit) { + // editText.clearFocus(); + // } + + // // Prevent default behavior except when we want it to insert a newline. + // return blurOnSubmit || !isMultiline; + // } + + // return true; + // } + // }); + } +} \ No newline at end of file