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 00000000000000..d67884677f12f9 Binary files /dev/null and b/RNTester/android/app/src/main/res/drawable/tv_banner.png differ diff --git a/React/Modules/RCTTVNavigationEventEmitter.m b/React/Modules/RCTTVNavigationEventEmitter.m index 7d6774b012ddbe..3ebe46381d1899 100644 --- a/React/Modules/RCTTVNavigationEventEmitter.m +++ b/React/Modules/RCTTVNavigationEventEmitter.m @@ -9,7 +9,7 @@ NSString *const RCTTVNavigationEventNotification = @"RCTTVNavigationEventNotification"; -static NSString *const TVNavigationEventName = @"onTVNavEvent"; +static NSString *const TVNavigationEventName = @"onHWKeyEvent"; @implementation RCTTVNavigationEventEmitter diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java index ffaeb08148cb22..00dee97df81cce 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java @@ -98,7 +98,7 @@ public void run() { mRecordingTestModule = new RecordingTestModule(); mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mRecordingTestModule) - .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new AndroidInfoModule(getContext())) .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java index d753d03f96dec2..4d58d579139d61 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java @@ -88,7 +88,7 @@ public void run() { jsModule = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(uiManager) - .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new AndroidInfoModule(getContext())) .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java index 38ed87b5d8c31f..70dd287164bdc1 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java @@ -80,7 +80,7 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mUIManager) - .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new AndroidInfoModule(getContext())) .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java index 11a2328021db36..73412bc21239d3 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java @@ -61,7 +61,7 @@ public void run() { mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(uiManager) - .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new AndroidInfoModule(getContext())) .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index ecda016133b275..c6a778a6745e55 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -79,7 +79,7 @@ public List 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,