From b7bb2e5745f2bdbfeeccef8d97d469730942e01c Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 6 Mar 2018 10:41:27 -0800 Subject: [PATCH] Add support for Android TV devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: * To be on par with Apple TV support, this makes it possible to run React Native apps on Android TV devices (See also: https://react-native.canny.io/feature-requests/p/android-tv-support) * These changes also make it possible to navigate through the app using D-PAD buttons that are present on some mobile devices * Since these changes affect, among others, `ReactRootView.java` and `Touchable.js` code and are closely related to Apple TV implementation, it makes sense for them to be included in the core - React native apps can be launched on Android TV devices and properly render their content - Navigation is possible using left, right, top, bottom arrows from the remote (or D-PAD) - Touchable components can handle D-PAD center button press events and correctly fire their `onPress` handlers - Touchable components will receive `onPressIn` and `onPressOut` events and can react to focus/blur changes appropriately (just like on Apple TV) - `Platform` constants allow to check if the react-native app is running on TV (`Platform.isTV`) - `ScrollView`s behave correctly (same as native implementation) when switching to view outside bounds – that is, the container would scroll such that the newly focused element is fully visible - Native "clicking" sounds are played when moving between focusable elements - Play/Pause click event is send to `TVEventHandler` - Rewind and FastForward events are send to `TVEventHandler` - Back button behaves as a normal Android back button - Diagonal buttons work correctly on Android TV, e.g. if there is no button directly to the right from the focused one, but there is one to the right but a bit higher/lower it will grab focus - Dev menu can be accessed by long pressing fast forward button A demo showing RNTester app running on Android TV device (Amazon Fire TV Stick) can be found here: [![RNAndroidTVDemo](http://img.youtube.com/vi/EzIQErHhY20/0.jpg)](http://www.youtube.com/watch?v=EzIQErHhY20) - `TextInput` will not work on Android TV devices. There's an issue with native `ReactEditText` implementation that prevents it from receiving focus. This makes it impossible to navigate to `TextInput`. This will be fixed next, but will be included in a separate Pull Request - ~Overlay permissions cannot be granted on Android TV devices running Android version >= 6.0 This is because the overlay permission can only be granted by firing an Intent to open settings page (`ACTION_MANAGE_OVERLAY_PERMISSION`). Since this page does not exist on TV devices the permission cannot be requested. This will make the app crash when trying to open dev menu (⌘+M) or displaying a redbox error. Note: This does not affect devices running Android version < 6.0 (for example Amazon Fire TV Stick)~ This is now fixed by: https://github.com/facebook/react-native/pull/16596 * Launch the RNTester app on Android TV device. * Ensure it launches without a crash * Ensure basic navigation is possible * Ensure Touchable components can receive select events * Ensure the changes do not break current Android and iOS mobile devices functionality. * Ensure the changes do not break current Apple TV functionality. [RNAndroidTVDemo video](http://img.youtube.com/vi/EzIQErHhY20/0.jpg) * Added `ReactAndroidTVViewManager` that handles TV `KeyEvent`s and dispatches events to JS - This is the core that enables basic navigation functionality on Android TV devices * Following the above change we copy `TVEventHandler.ios.js` into `TVEventHandler.android.js` to enable JS to pick up those native navigation events and dispatch them further to subscribed views. (Note: We do not have a native `TVNavigationEventEmitter` implementation on Android, thus this file is slightly modified, e.g. it does pass `null` to `NativeEventEmitter` constructor) * Added `uiMode` to `AndroidInfoModule`. (**Note**: This required changing `extends BaseJavaModule` to `extends ReactContextBaseJavaModule` to be able to use `getSystemService` which requires `Context` instance! * Added `isTV` constants to both `Platform.ios.js` (keeping the deprecated `isTVOS` as well) and `Platform.android.js` * Changed condition check on `Touchable.js` to use the newly added `isTV` flag to properly handle TV navigation events on Android as well * Added `LEANBACK_LAUNCHER` to `RNTester` `intent-filter` so that it is possible to launch it on Android TV devices. * See also a PR to `react-native-website` repo with updated docs for Android TV: https://github.com/facebook/react-native-website/pull/59 - [ ] Fix `TextInput` components handling by allowing them to be focused and making a proper navigation between them (and/or other components) possible. One thing to note here that the default behavior to immediately open software keyboard when focused on `TextInput` field will need to be adjusted on Android TV as well) - [x] Fix overlay permissions issue by changing the way redbox/dev menu are displayed (see: https://github.com/facebook/react-native/pull/16596) - [ ] Adjust placement of TV-related files (e.g. the `TVEventHandler.js` file is placed inside `AppleTV` directory which is not accurate, since it does handle Android TV events as well) Previous discussion: https://github.com/SoftwareMansion/react-native/pull/1 [ANDROID] [FEATURE] [TV] - Added support for Android TV devices Closes https://github.com/facebook/react-native/pull/16500 Differential Revision: D6536847 Pulled By: hramos fbshipit-source-id: 17bbb11e8583b97f195ced5fd9762f8902fb8a3d --- .../AppleTV/TVEventHandler.android.js | 18 ---- ...VEventHandler.ios.js => TVEventHandler.js} | 6 +- .../Components/AppleTV/TVViewPropTypes.js | 10 +- Libraries/Components/Button.js | 11 +- Libraries/Components/Touchable/Touchable.js | 4 +- .../TouchableNativeFeedback.android.js | 11 +- .../Components/Touchable/TouchableOpacity.js | 4 +- .../View/PlatformViewPropTypes.android.js | 11 -- ...pTypes.ios.js => PlatformViewPropTypes.js} | 5 +- Libraries/Utilities/Platform.android.js | 4 + Libraries/Utilities/Platform.ios.js | 6 ++ .../android/app/src/main/AndroidManifest.xml | 7 ++ .../app/src/main/res/drawable/tv_banner.png | Bin 0 -> 28950 bytes React/Modules/RCTTVNavigationEventEmitter.m | 2 +- ...alystNativeJSToJavaParametersTestCase.java | 2 +- .../tests/CatalystUIManagerTestCase.java | 2 +- .../react/tests/ProgressBarTestCase.java | 2 +- .../react/tests/ViewRenderingTestCase.java | 2 +- .../facebook/react/CoreModulesPackage.java | 2 +- .../com/facebook/react/ReactActivity.java | 10 ++ .../facebook/react/ReactActivityDelegate.java | 20 ++++ .../ReactAndroidHWInputDeviceHelper.java | 100 ++++++++++++++++++ .../com/facebook/react/ReactRootView.java | 59 +++++++++-- .../modules/systeminfo/AndroidInfoModule.java | 35 +++++- .../react/views/view/ReactViewManager.java | 12 ++- 25 files changed, 275 insertions(+), 70 deletions(-) delete mode 100644 Libraries/Components/AppleTV/TVEventHandler.android.js rename Libraries/Components/AppleTV/{TVEventHandler.ios.js => TVEventHandler.js} (91%) delete mode 100644 Libraries/Components/View/PlatformViewPropTypes.android.js rename Libraries/Components/View/{PlatformViewPropTypes.ios.js => PlatformViewPropTypes.js} (61%) create mode 100644 RNTester/android/app/src/main/res/drawable/tv_banner.png create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java diff --git a/Libraries/Components/AppleTV/TVEventHandler.android.js b/Libraries/Components/AppleTV/TVEventHandler.android.js deleted file mode 100644 index 718fa84a8de515..00000000000000 --- a/Libraries/Components/AppleTV/TVEventHandler.android.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @providesModule TVEventHandler - * @flow - */ -'use strict'; - -function TVEventHandler() {} - -TVEventHandler.prototype.enable = function(component: ?any, callback: Function) {}; - -TVEventHandler.prototype.disable = function() {}; - -module.exports = TVEventHandler; diff --git a/Libraries/Components/AppleTV/TVEventHandler.ios.js b/Libraries/Components/AppleTV/TVEventHandler.js similarity index 91% rename from Libraries/Components/AppleTV/TVEventHandler.ios.js rename to Libraries/Components/AppleTV/TVEventHandler.js index 94e67c23cf3532..363d50d903d876 100644 --- a/Libraries/Components/AppleTV/TVEventHandler.ios.js +++ b/Libraries/Components/AppleTV/TVEventHandler.js @@ -9,7 +9,7 @@ */ 'use strict'; -const React = require('React'); +const Platform = require('Platform'); const TVNavigationEventEmitter = require('NativeModules').TVNavigationEventEmitter; const NativeEventEmitter = require('NativeEventEmitter'); @@ -19,13 +19,13 @@ function TVEventHandler() { } TVEventHandler.prototype.enable = function(component: ?any, callback: Function) { - if (!TVNavigationEventEmitter) { + if (Platform.OS === 'ios' && !TVNavigationEventEmitter) { return; } this.__nativeTVNavigationEventEmitter = new NativeEventEmitter(TVNavigationEventEmitter); this.__nativeTVNavigationEventListener = this.__nativeTVNavigationEventEmitter.addListener( - 'onTVNavEvent', + 'onHWKeyEvent', (data) => { if (callback) { callback(component, data); diff --git a/Libraries/Components/AppleTV/TVViewPropTypes.js b/Libraries/Components/AppleTV/TVViewPropTypes.js index 90b86aa47d730b..2b6a6d4e53dcd1 100644 --- a/Libraries/Components/AppleTV/TVViewPropTypes.js +++ b/Libraries/Components/AppleTV/TVViewPropTypes.js @@ -15,17 +15,13 @@ const PropTypes = require('prop-types'); */ const TVViewPropTypes = { /** - * *(Apple TV only)* When set to true, this view will be focusable - * and navigable using the Apple TV remote. - * - * @platform ios + * When set to true, this view will be focusable + * and navigable using the TV remote. */ isTVSelectable: PropTypes.bool, /** - * *(Apple TV only)* May be set to true to force the Apple TV focus engine to move focus to this view. - * - * @platform ios + * May be set to true to force the TV focus engine to move focus to this view. */ hasTVPreferredFocus: PropTypes.bool, diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index dc5071f83193c9..4370f3571c103c 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -53,6 +53,7 @@ class Button extends React.Component<{ title: string, onPress: () => any, color?: ?string, + hasTVPreferredFocus?: ?boolean, accessibilityLabel?: ?string, disabled?: ?boolean, testID?: ?string, @@ -75,6 +76,10 @@ class Button extends React.Component<{ * If true, disable all interactions for this component. */ disabled: PropTypes.bool, + /** + * TV preferred focus (see documentation for the View component). + */ + hasTVPreferredFocus: PropTypes.bool, /** * Handler to be called when the user taps the button */ @@ -83,12 +88,6 @@ class Button extends React.Component<{ * Used to locate this view in end-to-end tests. */ testID: PropTypes.string, - /** - * *(Apple TV only)* TV preferred focus (see documentation for the View component). - * - * @platform ios - */ - hasTVPreferredFocus: PropTypes.bool, }; render() { diff --git a/Libraries/Components/Touchable/Touchable.js b/Libraries/Components/Touchable/Touchable.js index 9bfa38c6870a12..00a82cd64ac6e6 100644 --- a/Libraries/Components/Touchable/Touchable.js +++ b/Libraries/Components/Touchable/Touchable.js @@ -315,7 +315,7 @@ const LONG_PRESS_ALLOWED_MOVEMENT = 10; */ const TouchableMixin = { componentDidMount: function() { - if (!Platform.isTVOS) { + if (!Platform.isTV) { return; } @@ -329,7 +329,7 @@ const TouchableMixin = { } else if (evt.eventType === 'blur') { cmp.touchableHandleActivePressOut && cmp.touchableHandleActivePressOut(evt); } else if (evt.eventType === 'select') { - cmp.touchableHandlePress && cmp.touchableHandlePress(evt); + cmp.touchableHandlePress && !cmp.props.disabled && cmp.touchableHandlePress(evt); } } }); diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js index c4808b9bef23e3..4f3887642dce43 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -82,6 +82,11 @@ const TouchableNativeFeedback = createReactClass({ */ background: backgroundPropType, + /** + * TV preferred focus (see documentation for the View component). + */ + hasTVPreferredFocus: PropTypes.bool, + /** * Set to true to add the ripple effect to the foreground of the view, instead of the * background. This is useful if one of your child views has a background of its own, or you're @@ -156,7 +161,9 @@ const TouchableNativeFeedback = createReactClass({ touchableHandleActivePressIn: function(e: Event) { this.props.onPressIn && this.props.onPressIn(e); this._dispatchPressedStateChange(true); - this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY); + if (this.pressInLocation) { + this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY); + } }, touchableHandleActivePressOut: function(e: Event) { @@ -244,6 +251,8 @@ const TouchableNativeFeedback = createReactClass({ testID: this.props.testID, onLayout: this.props.onLayout, hitSlop: this.props.hitSlop, + isTVSelectable: true, + hasTVPreferredFocus: this.props.hasTVPreferredFocus, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, onResponderGrant: this.touchableHandleResponderGrant, diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index cbffbb59784972..8ff2ab8d0f2a34 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -128,9 +128,7 @@ const TouchableOpacity = createReactClass({ */ activeOpacity: PropTypes.number, /** - * *(Apple TV only)* TV preferred focus (see documentation for the View component). - * - * @platform ios + * TV preferred focus (see documentation for the View component). */ hasTVPreferredFocus: PropTypes.bool, /** diff --git a/Libraries/Components/View/PlatformViewPropTypes.android.js b/Libraries/Components/View/PlatformViewPropTypes.android.js deleted file mode 100644 index 64425f94110961..00000000000000 --- a/Libraries/Components/View/PlatformViewPropTypes.android.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @providesModule PlatformViewPropTypes - * @flow - */ - -module.export = {}; diff --git a/Libraries/Components/View/PlatformViewPropTypes.ios.js b/Libraries/Components/View/PlatformViewPropTypes.js similarity index 61% rename from Libraries/Components/View/PlatformViewPropTypes.ios.js rename to Libraries/Components/View/PlatformViewPropTypes.js index c3e4f6c6100de2..891fa34572a4f7 100644 --- a/Libraries/Components/View/PlatformViewPropTypes.ios.js +++ b/Libraries/Components/View/PlatformViewPropTypes.js @@ -11,7 +11,10 @@ const Platform = require('Platform'); let TVViewPropTypes = {}; -if (Platform.isTVOS) { +// We need to always include TVViewPropTypes on Android +// as unlike on iOS we can't detect TV devices at build time +// and hence make view manager export a different list of native properties. +if (Platform.isTV || Platform.OS === 'android') { TVViewPropTypes = require('TVViewPropTypes'); } diff --git a/Libraries/Utilities/Platform.android.js b/Libraries/Utilities/Platform.android.js index 006f371777ec86..f0e80633f2f283 100644 --- a/Libraries/Utilities/Platform.android.js +++ b/Libraries/Utilities/Platform.android.js @@ -22,6 +22,10 @@ const Platform = { const constants = NativeModules.PlatformConstants; return constants && constants.isTesting; }, + get isTV(): boolean { + const constants = NativeModules.PlatformConstants; + return constants && constants.uiMode === 'tv'; + }, select: (obj: Object) => 'android' in obj ? obj.android : obj.default, }; diff --git a/Libraries/Utilities/Platform.ios.js b/Libraries/Utilities/Platform.ios.js index 00bd3882651f83..776880b7fa400d 100644 --- a/Libraries/Utilities/Platform.ios.js +++ b/Libraries/Utilities/Platform.ios.js @@ -22,7 +22,13 @@ const Platform = { const constants = NativeModules.PlatformConstants; return constants ? constants.interfaceIdiom === 'pad' : false; }, + /** + * Deprecated, use `isTV` instead. + */ get isTVOS() { + return Platform.isTV; + }, + get isTV() { const constants = NativeModules.PlatformConstants; return constants ? constants.interfaceIdiom === 'tv' : false; }, diff --git a/RNTester/android/app/src/main/AndroidManifest.xml b/RNTester/android/app/src/main/AndroidManifest.xml index 818cdd422d54e7..0bde8735a3a8b2 100644 --- a/RNTester/android/app/src/main/AndroidManifest.xml +++ b/RNTester/android/app/src/main/AndroidManifest.xml @@ -20,9 +20,14 @@ android:minSdkVersion="16" android:targetSdkVersion="23" /> + @@ -34,6 +39,8 @@ + + diff --git a/RNTester/android/app/src/main/res/drawable/tv_banner.png b/RNTester/android/app/src/main/res/drawable/tv_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..d67884677f12f902cc74bed12c81b47474ce2852 GIT binary patch literal 28950 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 getNativeModules(final ReactApplicationContext reactCont new Provider() { @Override public NativeModule get() { - return new AndroidInfoModule(); + return new AndroidInfoModule(reactContext); } }), ModuleSpec.nativeModuleSpec( diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java index 3a8ca14b3ce8c7..bbe9cad4473816 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java @@ -75,11 +75,21 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { mDelegate.onActivityResult(requestCode, resultCode, data); } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return mDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); + } + @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); } + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return mDelegate.onKeyLongPress(keyCode, event) || super.onKeyLongPress(keyCode, event); + } + @Override public void onBackPressed() { if (!mDelegate.onBackPressed()) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index bee643c9bbc15e..03141fb00b07b2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -127,6 +127,16 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } } + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (getReactNativeHost().hasInstance() + && getReactNativeHost().getUseDeveloperSupport() + && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + event.startTracking(); + return true; + } + return false; + } + public boolean onKeyUp(int keyCode, KeyEvent event) { if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) { if (keyCode == KeyEvent.KEYCODE_MENU) { @@ -143,6 +153,16 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { return false; } + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + if (getReactNativeHost().hasInstance() + && getReactNativeHost().getUseDeveloperSupport() + && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); + return true; + } + return false; + } + public boolean onBackPressed() { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager().onBackPressed(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java new file mode 100644 index 00000000000000..8c3df938340e47 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react; + +import android.view.KeyEvent; +import android.view.View; + +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.MapBuilder; + +import java.util.Map; + +/** + * Responsible for dispatching events specific for hardware inputs. + */ +public class ReactAndroidHWInputDeviceHelper { + + /** + * Contains a mapping between handled KeyEvents and the corresponding navigation event + * that should be fired when the KeyEvent is received. + */ + private static final Map KEY_EVENTS_ACTIONS = MapBuilder.of( + KeyEvent.KEYCODE_DPAD_CENTER, + "select", + KeyEvent.KEYCODE_ENTER, + "select", + KeyEvent.KEYCODE_SPACE, + "select", + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, + "playPause", + KeyEvent.KEYCODE_MEDIA_REWIND, + "rewind", + KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, + "fastForward" + ); + + /** + * We keep a reference to the last focused view id + * so that we can send it as a target for key events + * and be able to send a blur event when focus changes. + */ + private int mLastFocusedViewId = View.NO_ID; + + private final ReactRootView mReactRootView; + + ReactAndroidHWInputDeviceHelper(ReactRootView mReactRootView) { + this.mReactRootView = mReactRootView; + } + + /** + * Called from {@link ReactRootView}. + * This is the main place the key events are handled. + */ + public void handleKeyEvent(KeyEvent ev) { + int eventKeyCode = ev.getKeyCode(); + int eventKeyAction = ev.getAction(); + if (eventKeyAction == KeyEvent.ACTION_UP && KEY_EVENTS_ACTIONS.containsKey(eventKeyCode)) { + dispatchEvent(KEY_EVENTS_ACTIONS.get(eventKeyCode), mLastFocusedViewId); + } + } + + /** + * Called from {@link ReactRootView} when focused view changes. + */ + public void onFocusChanged(View newFocusedView) { + if (mLastFocusedViewId == newFocusedView.getId()) { + return; + } + if (mLastFocusedViewId != View.NO_ID) { + dispatchEvent("blur", mLastFocusedViewId); + } + mLastFocusedViewId = newFocusedView.getId(); + dispatchEvent("focus", newFocusedView.getId()); + } + + /** + * Called from {@link ReactRootView} when the whole view hierarchy looses focus. + */ + public void clearFocus() { + if (mLastFocusedViewId != View.NO_ID) { + dispatchEvent("blur", mLastFocusedViewId); + } + mLastFocusedViewId = View.NO_ID; + } + + private void dispatchEvent(String eventType, int targetViewId) { + WritableMap event = new WritableNativeMap(); + event.putString("eventType", eventType); + if (targetViewId != View.NO_ID) { + event.putInt("tag", targetViewId); + } + mReactRootView.sendEvent("onHWKeyEvent", event); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 1bed2f5bb3b9bf..5d4a88bc5025c1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -16,6 +16,7 @@ import android.os.Bundle; import android.util.AttributeSet; import android.util.DisplayMetrics; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.View; @@ -84,6 +85,7 @@ public interface ReactRootViewEventListener { private boolean mIsAttachedToInstance; private boolean mShouldLogContentAppeared; private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this); + private final ReactAndroidHWInputDeviceHelper mAndroidHWInputDeviceHelper = new ReactAndroidHWInputDeviceHelper(this); private boolean mWasMeasured = false; private int mWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); private int mHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); @@ -213,6 +215,47 @@ protected void dispatchDraw(Canvas canvas) { } } + @Override + public boolean dispatchKeyEvent(KeyEvent ev) { + if (mReactInstanceManager == null || !mIsAttachedToInstance || + mReactInstanceManager.getCurrentReactContext() == null) { + FLog.w( + ReactConstants.TAG, + "Unable to handle key event as the catalyst instance has not been attached"); + return super.dispatchKeyEvent(ev); + } + mAndroidHWInputDeviceHelper.handleKeyEvent(ev); + return super.dispatchKeyEvent(ev); + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + if (mReactInstanceManager == null || !mIsAttachedToInstance || + mReactInstanceManager.getCurrentReactContext() == null) { + FLog.w( + ReactConstants.TAG, + "Unable to handle focus changed event as the catalyst instance has not been attached"); + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + return; + } + mAndroidHWInputDeviceHelper.clearFocus(); + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (mReactInstanceManager == null || !mIsAttachedToInstance || + mReactInstanceManager.getCurrentReactContext() == null) { + FLog.w( + ReactConstants.TAG, + "Unable to handle child focus changed event as the catalyst instance has not been attached"); + super.requestChildFocus(child, focused); + return; + } + mAndroidHWInputDeviceHelper.onFocusChanged(focused); + super.requestChildFocus(child, focused); + } + private void dispatchJSTouchEvent(MotionEvent event) { if (mReactInstanceManager == null || !mIsAttachedToInstance || mReactInstanceManager.getCurrentReactContext() == null) { @@ -536,6 +579,14 @@ public boolean isFabric() { public ReactInstanceManager getReactInstanceManager() { return mReactInstanceManager; } + + /* package */ void sendEvent(String eventName, @Nullable WritableMap params) { + if (mReactInstanceManager != null) { + mReactInstanceManager.getCurrentReactContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); + } + } private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener { private final Rect mVisibleViewArea; @@ -665,13 +716,5 @@ private void emitUpdateDimensionsEvent() { .getNativeModule(DeviceInfoModule.class) .emitUpdateDimensionsEvent(); } - - private void sendEvent(String eventName, @Nullable WritableMap params) { - if (mReactInstanceManager != null) { - mReactInstanceManager.getCurrentReactContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(eventName, params); - } - } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java index 98ca742f716803..1037d4c5ecbdcc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java @@ -7,9 +7,12 @@ package com.facebook.react.modules.systeminfo; +import android.app.UiModeManager; +import android.content.res.Configuration; import android.os.Build; -import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.module.annotations.ReactModule; import java.util.HashMap; @@ -17,14 +20,41 @@ import javax.annotation.Nullable; +import static android.content.Context.UI_MODE_SERVICE; + /** * Module that exposes Android Constants to JS. */ @ReactModule(name = "PlatformConstants") -public class AndroidInfoModule extends BaseJavaModule { +public class AndroidInfoModule extends ReactContextBaseJavaModule { private static final String IS_TESTING = "IS_TESTING"; + public AndroidInfoModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + /** + * See: https://developer.android.com/reference/android/app/UiModeManager.html#getCurrentModeType() + */ + private String uiMode() { + UiModeManager uiModeManager = (UiModeManager) getReactApplicationContext().getSystemService(UI_MODE_SERVICE); + switch (uiModeManager.getCurrentModeType()) { + case Configuration.UI_MODE_TYPE_TELEVISION: + return "tv"; + case Configuration.UI_MODE_TYPE_CAR: + return "car"; + case Configuration.UI_MODE_TYPE_DESK: + return "desk"; + case Configuration.UI_MODE_TYPE_WATCH: + return "watch"; + case Configuration.UI_MODE_TYPE_NORMAL: + return "normal"; + default: + return "unknown"; + } + } + @Override public String getName() { return "PlatformConstants"; @@ -41,6 +71,7 @@ public String getName() { constants.put("ServerHost", AndroidInfoHelpers.getServerHost()); constants.put("isTesting", "true".equals(System.getProperty(IS_TESTING))); constants.put("reactNativeVersion", ReactNativeVersion.VERSION); + constants.put("uiMode", uiMode()); return constants; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 0cd8f4ccf58b9c..b4eb587049bd9e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -56,8 +56,16 @@ public void setAccessible(ReactViewGroup view, boolean accessible) { view.setFocusable(accessible); } - @ReactPropGroup( - names = { + @ReactProp(name = "hasTVPreferredFocus") + public void setTVPreferredFocus(ReactViewGroup view, boolean hasTVPreferredFocus) { + if (hasTVPreferredFocus) { + view.setFocusable(true); + view.setFocusableInTouchMode(true); + view.requestFocus(); + } + } + + @ReactPropGroup(names = { ViewProps.BORDER_RADIUS, ViewProps.BORDER_TOP_LEFT_RADIUS, ViewProps.BORDER_TOP_RIGHT_RADIUS,