From 0e40e4f1606acae85b9e7433a1aefca2d2a2097b Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 10:01:54 +0200 Subject: [PATCH 01/48] Add LEANBACK_LAUNCHER to RNTester AndroidManifest This allows the app to be launched on Android TV devices as well --- RNTester/android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/RNTester/android/app/src/main/AndroidManifest.xml b/RNTester/android/app/src/main/AndroidManifest.xml index 818cdd422d54..0380b2890b58 100644 --- a/RNTester/android/app/src/main/AndroidManifest.xml +++ b/RNTester/android/app/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ + From 6dc6bb5d8a590d0eae4f9fc34b0bcb4c62927104 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 10:40:28 +0200 Subject: [PATCH 02/48] Add ReactAndroidTVViewManager Contains code for handling KeyEvents related to Android TV navigation --- .../androidtv/ReactAndroidTVViewManager.java | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVViewManager.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVViewManager.java new file mode 100644 index 000000000000..63ebbc39df42 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVViewManager.java @@ -0,0 +1,132 @@ +package com.facebook.react.views.androidtv; + +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +public class ReactAndroidTVViewManager extends ReactRootView { + + private static final List PRESS_KEY_EVENTS = Arrays.asList( + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_SPACE + ); + + private static final List NAVIGATION_KEY_EVENTS = Arrays.asList( + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_RIGHT + ); + + private + @Nullable ReactInstanceManager mReactInstanceManager; + + private View mLastFocusedView = null; + + public ReactAndroidTVViewManager(Context context) { + super(context); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent ev) { + int eventKeyCode = ev.getKeyCode(); + int eventKeyAction = ev.getAction(); + View targetView = getFocusedView(this); + if (targetView != null) { + if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { + handlePlayPauseEvent(); + } else if (PRESS_KEY_EVENTS.contains(eventKeyCode) && eventKeyAction == KeyEvent.ACTION_UP) { + handleSelectEvent(targetView); + } else if (NAVIGATION_KEY_EVENTS.contains(eventKeyCode)) { + handleFocusChangeEvent(targetView); + } + } + return super.dispatchKeyEvent(ev); + } + + private void handlePlayPauseEvent() { + dispatchEvent("playPause"); + } + + private void handleSelectEvent(View targetView) { + dispatchEvent("select", targetView); + } + + private void handleFocusChangeEvent(View targetView) { + if (mLastFocusedView == targetView) { + return; + } + if (mLastFocusedView != null) { + dispatchEvent("blur", mLastFocusedView); + } + mLastFocusedView = targetView; + dispatchEvent("focus", targetView); + } + + private void dispatchEvent(String eventType) { + dispatchEvent(eventType, null); + } + + private void dispatchEvent(String eventType, View targetView) { + WritableMap event = new WritableNativeMap(); + event.putString("eventType", eventType); + if (targetView != null) { + event.putInt("tag", targetView.getId()); + } + getEmitter().emit("onTVNavEvent", event); + } + + private DeviceEventManagerModule.RCTDeviceEventEmitter getEmitter() { + return mReactInstanceManager + .getCurrentReactContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + } + + private View getFocusedView(ViewGroup viewGroup) { + int childrenCount = viewGroup.getChildCount(); + for (int i = childrenCount - 1; i >= 0; i--) { + View view = viewGroup.getChildAt(i); + if (view.isFocused()) { + return view; + } + if (view instanceof ViewGroup) { + View nestedView = getFocusedView((ViewGroup) view); + if (nestedView != null) { + return nestedView; + } + } + } + return null; + } + + @Override + public void startReactApplication( + ReactInstanceManager reactInstanceManager, + String moduleName, + @Nullable Bundle initialProperties + ) { + super.startReactApplication(reactInstanceManager, moduleName, initialProperties); + mReactInstanceManager = reactInstanceManager; + } + + @Override + public void unmountReactApplication() { + mReactInstanceManager = null; + super.unmountReactApplication(); + } + +} From 8a71318759db26d663a3687ca7313127235942f5 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 10:53:59 +0200 Subject: [PATCH 03/48] Add isRunningOnTv flag to AndroidInfoModule constants --- .../com/facebook/react/CoreModulesPackage.java | 2 +- .../modules/systeminfo/AndroidInfoHelpers.java | 13 ++++++++++++- .../modules/systeminfo/AndroidInfoModule.java | 14 ++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 7b0ac3253bf4..b8cba9727937 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -81,7 +81,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/modules/systeminfo/AndroidInfoHelpers.java b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java index bbe0c07695d0..c02a89f51e5a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java @@ -2,9 +2,15 @@ package com.facebook.react.modules.systeminfo; +import android.app.UiModeManager; +import android.content.res.Configuration; +import android.os.Build; + +import com.facebook.react.bridge.ReactContext; + import java.util.Locale; -import android.os.Build; +import static android.content.Context.UI_MODE_SERVICE; public class AndroidInfoHelpers { @@ -56,4 +62,9 @@ private static String getServerIpAddress(int port) { return String.format(Locale.US, "%s:%d", ipAddress, port); } + + public static boolean isRunningOnTV(ReactContext reactContext) { + UiModeManager uiModeManager = (UiModeManager) reactContext.getSystemService(UI_MODE_SERVICE); + return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; + } } 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 700b6a616bb4..b1db73865434 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 @@ -11,7 +11,9 @@ import android.os.Build; -import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.module.annotations.ReactModule; import java.util.HashMap; @@ -23,10 +25,17 @@ * 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"; + private ReactContext mReactContext; + + public AndroidInfoModule(ReactApplicationContext reactContext) { + super(reactContext); + this.mReactContext = reactContext; + } + @Override public String getName() { return "PlatformConstants"; @@ -43,6 +52,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("isRunningOnTV", AndroidInfoHelpers.isRunningOnTV(mReactContext)); return constants; } } From 7788de1c721577912723edd9de1cd0b024d689ef Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 10:57:50 +0200 Subject: [PATCH 04/48] Expose isTV flag in Platform constants --- Libraries/Utilities/Platform.android.js | 4 ++++ Libraries/Utilities/Platform.ios.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Libraries/Utilities/Platform.android.js b/Libraries/Utilities/Platform.android.js index 4bab19c52872..eece47118d58 100644 --- a/Libraries/Utilities/Platform.android.js +++ b/Libraries/Utilities/Platform.android.js @@ -24,6 +24,10 @@ const Platform = { const constants = NativeModules.PlatformConstants; return constants && constants.isTesting; }, + get isTV(): boolean { + const constants = NativeModules.PlatformConstants; + return constants && constants.isRunningOnTV; + }, 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 3238a44627fe..e0eb9ee50a24 100644 --- a/Libraries/Utilities/Platform.ios.js +++ b/Libraries/Utilities/Platform.ios.js @@ -28,6 +28,9 @@ const Platform = { const constants = NativeModules.PlatformConstants; return constants ? constants.interfaceIdiom === 'tv' : false; }, + get isTV() { + return Platform.isTVOS(); + }, get isTesting(): boolean { const constants = NativeModules.PlatformConstants; return constants && constants.isTesting; From 29eb058d0a3cc4b3c98ac058e9d5e869739d1e16 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 11:02:52 +0200 Subject: [PATCH 05/48] Copy iOS TVEventHandler implementation over to Android --- .../AppleTV/TVEventHandler.android.js | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Libraries/Components/AppleTV/TVEventHandler.android.js b/Libraries/Components/AppleTV/TVEventHandler.android.js index dc5a1d6b94e9..d5e5d4b74238 100644 --- a/Libraries/Components/AppleTV/TVEventHandler.android.js +++ b/Libraries/Components/AppleTV/TVEventHandler.android.js @@ -11,10 +11,35 @@ */ 'use strict'; -function TVEventHandler() {} +const React = require('React'); +const NativeEventEmitter = require('NativeEventEmitter'); -TVEventHandler.prototype.enable = function(component: ?any, callback: Function) {}; +function TVEventHandler() { + this.__nativeTVNavigationEventListener = null; + this.__nativeTVNavigationEventEmitter = null; +} -TVEventHandler.prototype.disable = function() {}; +TVEventHandler.prototype.enable = function(component: ?any, callback: Function) { + this.__nativeTVNavigationEventEmitter = new NativeEventEmitter(null); + this.__nativeTVNavigationEventListener = this.__nativeTVNavigationEventEmitter.addListener( + 'onTVNavEvent', + (data) => { + if (callback) { + callback(component, data); + } + } + ); +}; + +TVEventHandler.prototype.disable = function() { + if (this.__nativeTVNavigationEventListener) { + this.__nativeTVNavigationEventListener.remove(); + delete this.__nativeTVNavigationEventListener; + } + if (this.__nativeTVNavigationEventEmitter) { + delete this.__nativeTVNavigationEventEmitter; + } +}; module.exports = TVEventHandler; + From bf86743e439c44d8783554c8bcc1dab10e7deeb7 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 11:04:40 +0200 Subject: [PATCH 06/48] Change TV check in Touchable component --- Libraries/Components/Touchable/Touchable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Components/Touchable/Touchable.js b/Libraries/Components/Touchable/Touchable.js index e8f925e90d5b..a03bfa459c5a 100644 --- a/Libraries/Components/Touchable/Touchable.js +++ b/Libraries/Components/Touchable/Touchable.js @@ -317,7 +317,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10; */ var TouchableMixin = { componentDidMount: function() { - if (!Platform.isTVOS) { + if (!Platform.isTV) { return; } From 2eac232387221043deb52c3f8148fd8f495ed5a1 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 11:17:38 +0200 Subject: [PATCH 07/48] Use ReactAndroidTVViewManager as default RootView --- .../main/java/com/facebook/react/ReactActivityDelegate.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index bee643c9bbc1..36705b535adf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -16,6 +16,7 @@ import com.facebook.react.devsupport.DoubleTapReloadRecognizer; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.PermissionListener; +import com.facebook.react.views.androidtv.ReactAndroidTVViewManager; import javax.annotation.Nullable; @@ -54,7 +55,7 @@ public ReactActivityDelegate( } protected ReactRootView createRootView() { - return new ReactRootView(getContext()); + return new ReactAndroidTVViewManager(getContext()); } /** From b961f2d8f2afddc3bc17b512bea02495fe0abed5 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 13:35:04 +0200 Subject: [PATCH 08/48] Prevent TouchableNativeFeedback from crashing on Android TV --- .../Components/Touchable/TouchableNativeFeedback.android.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js index f5e43340e043..18b4329243ee 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -158,7 +158,9 @@ var 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 (!Platform.isTV) { + this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY); + } }, touchableHandleActivePressOut: function(e: Event) { From 51df826eb6b99a19de4597e80ec2631dfd7a5383 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 14:00:50 +0200 Subject: [PATCH 09/48] Prevent crash when asking for overlay permission on Android TV --- .../main/java/com/facebook/react/ReactActivityDelegate.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 36705b535adf..641f87ec62da 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -80,6 +80,11 @@ protected void onCreate(Bundle savedInstanceState) { mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); } + private boolean canHandleIntent(Intent intent) { + PackageManager packageManager = getContext().getPackageManager(); + return intent.resolveActivity(packageManager) != null; + } + protected void loadApp(String appKey) { if (mReactRootView != null) { throw new IllegalStateException("Cannot loadApp while app is already running."); From d8c0f644cc2b2d5de47412f10d914a8dc0fa4211 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 16:05:37 +0200 Subject: [PATCH 10/48] Decouple ReactAvtivityDelegate from ReactAndroidTVRootViewHelper Instead of creating TV-specific root view handle the `dispatchKeyEvent` in ReactRootView and proxy it to ReactAndroidTVRootViewHelper to handle --- .../facebook/react/ReactActivityDelegate.java | 4 +- .../com/facebook/react/ReactRootView.java | 18 +++++ ...java => ReactAndroidTVRootViewHelper.java} | 75 ++++++------------- 3 files changed, 42 insertions(+), 55 deletions(-) rename ReactAndroid/src/main/java/com/facebook/react/views/androidtv/{ReactAndroidTVViewManager.java => ReactAndroidTVRootViewHelper.java} (51%) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 641f87ec62da..6a0d47e8edc9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -16,7 +16,7 @@ import com.facebook.react.devsupport.DoubleTapReloadRecognizer; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.PermissionListener; -import com.facebook.react.views.androidtv.ReactAndroidTVViewManager; +import com.facebook.react.views.androidtv.ReactAndroidTVRootViewHelper; import javax.annotation.Nullable; @@ -55,7 +55,7 @@ public ReactActivityDelegate( } protected ReactRootView createRootView() { - return new ReactAndroidTVViewManager(getContext()); + return new ReactRootView(getContext()); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index c67cab536cd6..2eafc408c85c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -18,6 +18,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; @@ -49,6 +50,7 @@ import com.facebook.react.uimanager.SizeMonitoringFrameLayout; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.androidtv.ReactAndroidTVRootViewHelper; import com.facebook.systrace.Systrace; import javax.annotation.Nullable; @@ -86,6 +88,7 @@ public interface ReactRootViewEventListener { private boolean mIsAttachedToInstance; private boolean mShouldLogContentAppeared; private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this); + private final ReactAndroidTVRootViewHelper mAndroidTVRootViewHelper = new ReactAndroidTVRootViewHelper(this); private boolean mWasMeasured = false; private int mWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); private int mHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); @@ -214,6 +217,21 @@ 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); + } + ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + DeviceEventManagerModule.RCTDeviceEventEmitter eventEmitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + mAndroidTVRootViewHelper.handleKeyEvent(ev, eventEmitter); + return super.dispatchKeyEvent(ev); + } + private void dispatchJSTouchEvent(MotionEvent event) { if (mReactInstanceManager == null || !mIsAttachedToInstance || mReactInstanceManager.getCurrentReactContext() == null) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java similarity index 51% rename from ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVViewManager.java rename to ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java index 63ebbc39df42..dfec2868bbeb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java @@ -1,23 +1,18 @@ package com.facebook.react.views.androidtv; -import android.content.Context; -import android.os.Bundle; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; -import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactRootView; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; import java.util.Arrays; import java.util.List; -import javax.annotation.Nullable; - -public class ReactAndroidTVViewManager extends ReactRootView { +public class ReactAndroidTVRootViewHelper { private static final List PRESS_KEY_EVENTS = Arrays.asList( KeyEvent.KEYCODE_DPAD_CENTER, @@ -32,68 +27,59 @@ public class ReactAndroidTVViewManager extends ReactRootView { KeyEvent.KEYCODE_DPAD_RIGHT ); - private - @Nullable ReactInstanceManager mReactInstanceManager; - private View mLastFocusedView = null; - public ReactAndroidTVViewManager(Context context) { - super(context); + private ReactRootView mReactRootView; + + public ReactAndroidTVRootViewHelper(ReactRootView mReactRootView) { + this.mReactRootView = mReactRootView; } - @Override - public boolean dispatchKeyEvent(KeyEvent ev) { + public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { int eventKeyCode = ev.getKeyCode(); int eventKeyAction = ev.getAction(); - View targetView = getFocusedView(this); + View targetView = getFocusedView(mReactRootView); if (targetView != null) { if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { - handlePlayPauseEvent(); + handlePlayPauseEvent(emitter); } else if (PRESS_KEY_EVENTS.contains(eventKeyCode) && eventKeyAction == KeyEvent.ACTION_UP) { - handleSelectEvent(targetView); + handleSelectEvent(targetView, emitter); } else if (NAVIGATION_KEY_EVENTS.contains(eventKeyCode)) { - handleFocusChangeEvent(targetView); + handleFocusChangeEvent(targetView, emitter); } } - return super.dispatchKeyEvent(ev); } - private void handlePlayPauseEvent() { - dispatchEvent("playPause"); + private void handlePlayPauseEvent(RCTDeviceEventEmitter emitter) { + dispatchEvent("playPause", emitter); } - private void handleSelectEvent(View targetView) { - dispatchEvent("select", targetView); + private void handleSelectEvent(View targetView, RCTDeviceEventEmitter emitter) { + dispatchEvent("select", targetView, emitter); } - private void handleFocusChangeEvent(View targetView) { + private void handleFocusChangeEvent(View targetView, RCTDeviceEventEmitter emitter) { if (mLastFocusedView == targetView) { return; } if (mLastFocusedView != null) { - dispatchEvent("blur", mLastFocusedView); + dispatchEvent("blur", mLastFocusedView, emitter); } mLastFocusedView = targetView; - dispatchEvent("focus", targetView); + dispatchEvent("focus", targetView, emitter); } - private void dispatchEvent(String eventType) { - dispatchEvent(eventType, null); + private void dispatchEvent(String eventType, RCTDeviceEventEmitter emitter) { + dispatchEvent(eventType, null, emitter); } - private void dispatchEvent(String eventType, View targetView) { + private void dispatchEvent(String eventType, View targetView, RCTDeviceEventEmitter emitter) { WritableMap event = new WritableNativeMap(); event.putString("eventType", eventType); if (targetView != null) { event.putInt("tag", targetView.getId()); } - getEmitter().emit("onTVNavEvent", event); - } - - private DeviceEventManagerModule.RCTDeviceEventEmitter getEmitter() { - return mReactInstanceManager - .getCurrentReactContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + emitter.emit("onTVNavEvent", event); } private View getFocusedView(ViewGroup viewGroup) { @@ -112,21 +98,4 @@ private View getFocusedView(ViewGroup viewGroup) { } return null; } - - @Override - public void startReactApplication( - ReactInstanceManager reactInstanceManager, - String moduleName, - @Nullable Bundle initialProperties - ) { - super.startReactApplication(reactInstanceManager, moduleName, initialProperties); - mReactInstanceManager = reactInstanceManager; - } - - @Override - public void unmountReactApplication() { - mReactInstanceManager = null; - super.unmountReactApplication(); - } - } From 4a75d81d37308d7355e56f6a19881a5577d0fc1e Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 16 Oct 2017 16:17:41 +0200 Subject: [PATCH 11/48] Add comment to RNTester Manifest for leanback launcher --- RNTester/android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/RNTester/android/app/src/main/AndroidManifest.xml b/RNTester/android/app/src/main/AndroidManifest.xml index 0380b2890b58..0663f5ed8c63 100644 --- a/RNTester/android/app/src/main/AndroidManifest.xml +++ b/RNTester/android/app/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ + From 5e7a6cdd205ef6641996924f26f60951a2e7ca0b Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 08:31:49 +0200 Subject: [PATCH 12/48] Change isRunningOnTV to uiMode --- Libraries/Utilities/Platform.android.js | 2 +- .../systeminfo/AndroidInfoHelpers.java | 13 +--------- .../modules/systeminfo/AndroidInfoModule.java | 24 ++++++++++++++++++- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Libraries/Utilities/Platform.android.js b/Libraries/Utilities/Platform.android.js index eece47118d58..143773b8c173 100644 --- a/Libraries/Utilities/Platform.android.js +++ b/Libraries/Utilities/Platform.android.js @@ -26,7 +26,7 @@ const Platform = { }, get isTV(): boolean { const constants = NativeModules.PlatformConstants; - return constants && constants.isRunningOnTV; + return constants && constants.uiMode === 'tv'; }, select: (obj: Object) => 'android' in obj ? obj.android : obj.default, }; diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java index c02a89f51e5a..bbe0c07695d0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.java @@ -2,15 +2,9 @@ package com.facebook.react.modules.systeminfo; -import android.app.UiModeManager; -import android.content.res.Configuration; -import android.os.Build; - -import com.facebook.react.bridge.ReactContext; - import java.util.Locale; -import static android.content.Context.UI_MODE_SERVICE; +import android.os.Build; public class AndroidInfoHelpers { @@ -62,9 +56,4 @@ private static String getServerIpAddress(int port) { return String.format(Locale.US, "%s:%d", ipAddress, port); } - - public static boolean isRunningOnTV(ReactContext reactContext) { - UiModeManager uiModeManager = (UiModeManager) reactContext.getSystemService(UI_MODE_SERVICE); - return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; - } } 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 b1db73865434..b3c6e8ff8718 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 @@ -9,6 +9,8 @@ package com.facebook.react.modules.systeminfo; +import android.app.UiModeManager; +import android.content.res.Configuration; import android.os.Build; import com.facebook.react.bridge.ReactApplicationContext; @@ -21,6 +23,8 @@ import javax.annotation.Nullable; +import static android.content.Context.UI_MODE_SERVICE; + /** * Module that exposes Android Constants to JS. */ @@ -36,6 +40,24 @@ public AndroidInfoModule(ReactApplicationContext reactContext) { this.mReactContext = reactContext; } + private String uiMode() { + UiModeManager uiModeManager = (UiModeManager) mReactContext.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"; @@ -52,7 +74,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("isRunningOnTV", AndroidInfoHelpers.isRunningOnTV(mReactContext)); + constants.put("uiMode", uiMode()); return constants; } } From c4218f61aa68dd41fde4cf89c29b604a5c3feb7c Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 13:56:00 +0200 Subject: [PATCH 13/48] Change condition check to dispatch hotspot update --- .../Components/Touchable/TouchableNativeFeedback.android.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js index 18b4329243ee..bb20038f33c0 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -158,7 +158,7 @@ var TouchableNativeFeedback = createReactClass({ touchableHandleActivePressIn: function(e: Event) { this.props.onPressIn && this.props.onPressIn(e); this._dispatchPressedStateChange(true); - if (!Platform.isTV) { + if (this.pressInLocation) { this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY); } }, From a5a7b3bda88c85111fab71e39966065b02bf6051 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 13:57:16 +0200 Subject: [PATCH 14/48] Return true after dispatchKeyEvent --- .../src/main/java/com/facebook/react/ReactRootView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 2eafc408c85c..3be985795986 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -229,7 +229,7 @@ public boolean dispatchKeyEvent(KeyEvent ev) { ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); DeviceEventManagerModule.RCTDeviceEventEmitter eventEmitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); mAndroidTVRootViewHelper.handleKeyEvent(ev, eventEmitter); - return super.dispatchKeyEvent(ev); + return true; } private void dispatchJSTouchEvent(MotionEvent event) { From beb738f487d443685566c065782dc97bead4cff3 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 14:07:09 +0200 Subject: [PATCH 15/48] Fix isTV Platform constants on iOS --- Libraries/Utilities/Platform.ios.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Utilities/Platform.ios.js b/Libraries/Utilities/Platform.ios.js index e0eb9ee50a24..8aeaa9ec90f8 100644 --- a/Libraries/Utilities/Platform.ios.js +++ b/Libraries/Utilities/Platform.ios.js @@ -29,7 +29,7 @@ const Platform = { return constants ? constants.interfaceIdiom === 'tv' : false; }, get isTV() { - return Platform.isTVOS(); + return Platform.isTVOS; }, get isTesting(): boolean { const constants = NativeModules.PlatformConstants; From ac9d5844365d91dc06eb31b8fbb2498a7eeba048 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 14:07:32 +0200 Subject: [PATCH 16/48] Consolidate TVEventHandler to single file --- .../AppleTV/TVEventHandler.android.js | 45 ------------------- ...VEventHandler.ios.js => TVEventHandler.js} | 3 +- 2 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 Libraries/Components/AppleTV/TVEventHandler.android.js rename Libraries/Components/AppleTV/{TVEventHandler.ios.js => TVEventHandler.js} (93%) diff --git a/Libraries/Components/AppleTV/TVEventHandler.android.js b/Libraries/Components/AppleTV/TVEventHandler.android.js deleted file mode 100644 index d5e5d4b74238..000000000000 --- a/Libraries/Components/AppleTV/TVEventHandler.android.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule TVEventHandler - * @flow - */ -'use strict'; - -const React = require('React'); -const NativeEventEmitter = require('NativeEventEmitter'); - -function TVEventHandler() { - this.__nativeTVNavigationEventListener = null; - this.__nativeTVNavigationEventEmitter = null; -} - -TVEventHandler.prototype.enable = function(component: ?any, callback: Function) { - this.__nativeTVNavigationEventEmitter = new NativeEventEmitter(null); - this.__nativeTVNavigationEventListener = this.__nativeTVNavigationEventEmitter.addListener( - 'onTVNavEvent', - (data) => { - if (callback) { - callback(component, data); - } - } - ); -}; - -TVEventHandler.prototype.disable = function() { - if (this.__nativeTVNavigationEventListener) { - this.__nativeTVNavigationEventListener.remove(); - delete this.__nativeTVNavigationEventListener; - } - if (this.__nativeTVNavigationEventEmitter) { - delete this.__nativeTVNavigationEventEmitter; - } -}; - -module.exports = TVEventHandler; - diff --git a/Libraries/Components/AppleTV/TVEventHandler.ios.js b/Libraries/Components/AppleTV/TVEventHandler.js similarity index 93% rename from Libraries/Components/AppleTV/TVEventHandler.ios.js rename to Libraries/Components/AppleTV/TVEventHandler.js index c01f2b065a76..1a2a2edbd9e5 100644 --- a/Libraries/Components/AppleTV/TVEventHandler.ios.js +++ b/Libraries/Components/AppleTV/TVEventHandler.js @@ -12,6 +12,7 @@ 'use strict'; const React = require('React'); +const Platform = require('Platform'); const TVNavigationEventEmitter = require('NativeModules').TVNavigationEventEmitter; const NativeEventEmitter = require('NativeEventEmitter'); @@ -21,7 +22,7 @@ function TVEventHandler() { } TVEventHandler.prototype.enable = function(component: ?any, callback: Function) { - if (!TVNavigationEventEmitter) { + if (Platform.OS === 'ios' && !TVNavigationEventEmitter) { return; } From 318218f299890ef1a79c2b40efe4ad60f61fcf00 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 14:49:18 +0200 Subject: [PATCH 17/48] Remove mReactContext local variable from AndroidInfoModule --- .../react/modules/systeminfo/AndroidInfoModule.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 b3c6e8ff8718..f077730d1d99 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 @@ -14,7 +14,6 @@ import android.os.Build; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.module.annotations.ReactModule; @@ -33,15 +32,12 @@ public class AndroidInfoModule extends ReactContextBaseJavaModule { private static final String IS_TESTING = "IS_TESTING"; - private ReactContext mReactContext; - public AndroidInfoModule(ReactApplicationContext reactContext) { super(reactContext); - this.mReactContext = reactContext; } private String uiMode() { - UiModeManager uiModeManager = (UiModeManager) mReactContext.getSystemService(UI_MODE_SERVICE); + UiModeManager uiModeManager = (UiModeManager) getReactApplicationContext().getSystemService(UI_MODE_SERVICE); switch (uiModeManager.getCurrentModeType()) { case Configuration.UI_MODE_TYPE_TELEVISION: return "tv"; From 2c37223d9c7846e12d2b59ddfb0d3ddf8eec2055 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 14:52:45 +0200 Subject: [PATCH 18/48] Minor renamed in ReactAndroidTVRootViewHelper --- .../views/androidtv/ReactAndroidTVRootViewHelper.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java index dfec2868bbeb..db23caa05814 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java @@ -31,14 +31,14 @@ public class ReactAndroidTVRootViewHelper { private ReactRootView mReactRootView; - public ReactAndroidTVRootViewHelper(ReactRootView mReactRootView) { - this.mReactRootView = mReactRootView; + public ReactAndroidTVRootViewHelper(ReactRootView reactRootView) { + mReactRootView = reactRootView; } public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { int eventKeyCode = ev.getKeyCode(); int eventKeyAction = ev.getAction(); - View targetView = getFocusedView(mReactRootView); + View targetView = findFocusedView(mReactRootView); if (targetView != null) { if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { handlePlayPauseEvent(emitter); @@ -82,7 +82,7 @@ private void dispatchEvent(String eventType, View targetView, RCTDeviceEventEmit emitter.emit("onTVNavEvent", event); } - private View getFocusedView(ViewGroup viewGroup) { + private View findFocusedView(ViewGroup viewGroup) { int childrenCount = viewGroup.getChildCount(); for (int i = childrenCount - 1; i >= 0; i--) { View view = viewGroup.getChildAt(i); @@ -90,7 +90,7 @@ private View getFocusedView(ViewGroup viewGroup) { return view; } if (view instanceof ViewGroup) { - View nestedView = getFocusedView((ViewGroup) view); + View nestedView = findFocusedView((ViewGroup) view); if (nestedView != null) { return nestedView; } From 4aeca359cf5e9e1e5077e83e81a20db267399519 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 14:54:31 +0200 Subject: [PATCH 19/48] Prevent sending select event if target component is disabled --- Libraries/Components/Touchable/Touchable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Components/Touchable/Touchable.js b/Libraries/Components/Touchable/Touchable.js index a03bfa459c5a..40b2032f81e5 100644 --- a/Libraries/Components/Touchable/Touchable.js +++ b/Libraries/Components/Touchable/Touchable.js @@ -331,7 +331,7 @@ var 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); } } }); From 9e042431608efbe7a8fac265ed5b5dd1e4770030 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 17 Oct 2017 15:43:19 +0200 Subject: [PATCH 20/48] Revert "Return true after dispatchKeyEvent" This reverts commit 0a7f21804a551a246cd90fd881b6afb9db48690d. --- .../src/main/java/com/facebook/react/ReactRootView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 3be985795986..2eafc408c85c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -229,7 +229,7 @@ public boolean dispatchKeyEvent(KeyEvent ev) { ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); DeviceEventManagerModule.RCTDeviceEventEmitter eventEmitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); mAndroidTVRootViewHelper.handleKeyEvent(ev, eventEmitter); - return true; + return super.dispatchKeyEvent(ev); } private void dispatchJSTouchEvent(MotionEvent event) { From 1a68b10e88b8f4be34dd4c3453cbd4fa2d1f3278 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Thu, 19 Oct 2017 12:42:04 +0200 Subject: [PATCH 21/48] Handle focus change events in ReactRootView --- .../com/facebook/react/ReactRootView.java | 37 ++++++++++++-- .../ReactAndroidTVRootViewHelper.java | 50 +++++++++---------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 2eafc408c85c..f881a8712b37 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -226,12 +226,43 @@ public boolean dispatchKeyEvent(KeyEvent ev) { "Unable to handle key event as the catalyst instance has not been attached"); return super.dispatchKeyEvent(ev); } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); - DeviceEventManagerModule.RCTDeviceEventEmitter eventEmitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); - mAndroidTVRootViewHelper.handleKeyEvent(ev, eventEmitter); + mAndroidTVRootViewHelper.handleKeyEvent(ev, getDeviceEventEmitter()); 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; + } + mAndroidTVRootViewHelper.clearFocus(getDeviceEventEmitter()); + 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; + } + mAndroidTVRootViewHelper.onFocusChanged(focused, getDeviceEventEmitter()); + super.requestChildFocus(child, focused); + } + + private DeviceEventManagerModule.RCTDeviceEventEmitter getDeviceEventEmitter() { + ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + return reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + } + private void dispatchJSTouchEvent(MotionEvent event) { if (mReactInstanceManager == null || !mIsAttachedToInstance || mReactInstanceManager.getCurrentReactContext() == null) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java index db23caa05814..06161bbff7c3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java @@ -20,14 +20,7 @@ public class ReactAndroidTVRootViewHelper { KeyEvent.KEYCODE_SPACE ); - private static final List NAVIGATION_KEY_EVENTS = Arrays.asList( - KeyEvent.KEYCODE_DPAD_DOWN, - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_UP, - KeyEvent.KEYCODE_DPAD_RIGHT - ); - - private View mLastFocusedView = null; + private int mLastFocusedViewId = View.NO_ID; private ReactRootView mReactRootView; @@ -44,40 +37,45 @@ public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { handlePlayPauseEvent(emitter); } else if (PRESS_KEY_EVENTS.contains(eventKeyCode) && eventKeyAction == KeyEvent.ACTION_UP) { handleSelectEvent(targetView, emitter); - } else if (NAVIGATION_KEY_EVENTS.contains(eventKeyCode)) { - handleFocusChangeEvent(targetView, emitter); } } } + public void onFocusChanged(View newFocusedView, RCTDeviceEventEmitter emitter) { + if (mLastFocusedViewId == newFocusedView.getId()) { + return; + } + if (mLastFocusedViewId != View.NO_ID) { + dispatchEvent("blur", mLastFocusedViewId, emitter); + } + mLastFocusedViewId = newFocusedView.getId(); + dispatchEvent("focus", newFocusedView.getId(), emitter); + } + + public void clearFocus(RCTDeviceEventEmitter emitter) { + if (mLastFocusedViewId != View.NO_ID) { + dispatchEvent("blur", mLastFocusedViewId, emitter); + } + mLastFocusedViewId = View.NO_ID; + } + private void handlePlayPauseEvent(RCTDeviceEventEmitter emitter) { dispatchEvent("playPause", emitter); } private void handleSelectEvent(View targetView, RCTDeviceEventEmitter emitter) { - dispatchEvent("select", targetView, emitter); - } - - private void handleFocusChangeEvent(View targetView, RCTDeviceEventEmitter emitter) { - if (mLastFocusedView == targetView) { - return; - } - if (mLastFocusedView != null) { - dispatchEvent("blur", mLastFocusedView, emitter); - } - mLastFocusedView = targetView; - dispatchEvent("focus", targetView, emitter); + dispatchEvent("select", targetView.getId(), emitter); } private void dispatchEvent(String eventType, RCTDeviceEventEmitter emitter) { - dispatchEvent(eventType, null, emitter); + dispatchEvent(eventType, View.NO_ID, emitter); } - private void dispatchEvent(String eventType, View targetView, RCTDeviceEventEmitter emitter) { + private void dispatchEvent(String eventType, int targetViewId, RCTDeviceEventEmitter emitter) { WritableMap event = new WritableNativeMap(); event.putString("eventType", eventType); - if (targetView != null) { - event.putInt("tag", targetView.getId()); + if (targetViewId != View.NO_ID) { + event.putInt("tag", targetViewId); } emitter.emit("onTVNavEvent", event); } From 8ea9950f20bb05efeac44e11dce1d6d543e2c9ea Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Fri, 20 Oct 2017 10:33:28 +0200 Subject: [PATCH 22/48] Add launcher banner icon --- .../android/app/src/main/AndroidManifest.xml | 5 +++++ .../app/src/main/res/drawable/tv_banner.png | Bin 0 -> 28950 bytes 2 files changed, 5 insertions(+) create mode 100644 RNTester/android/app/src/main/res/drawable/tv_banner.png diff --git a/RNTester/android/app/src/main/AndroidManifest.xml b/RNTester/android/app/src/main/AndroidManifest.xml index 0663f5ed8c63..0bde8735a3a8 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" /> + 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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 Date: Fri, 20 Oct 2017 10:34:41 +0200 Subject: [PATCH 23/48] Inline keyEvent handler methods --- .../androidtv/ReactAndroidTVRootViewHelper.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java index 06161bbff7c3..d06ca3c1c115 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java @@ -34,9 +34,9 @@ public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { View targetView = findFocusedView(mReactRootView); if (targetView != null) { if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { - handlePlayPauseEvent(emitter); + dispatchEvent("playPause", emitter); } else if (PRESS_KEY_EVENTS.contains(eventKeyCode) && eventKeyAction == KeyEvent.ACTION_UP) { - handleSelectEvent(targetView, emitter); + dispatchEvent("select", targetView.getId(), emitter); } } } @@ -59,14 +59,6 @@ public void clearFocus(RCTDeviceEventEmitter emitter) { mLastFocusedViewId = View.NO_ID; } - private void handlePlayPauseEvent(RCTDeviceEventEmitter emitter) { - dispatchEvent("playPause", emitter); - } - - private void handleSelectEvent(View targetView, RCTDeviceEventEmitter emitter) { - dispatchEvent("select", targetView.getId(), emitter); - } - private void dispatchEvent(String eventType, RCTDeviceEventEmitter emitter) { dispatchEvent(eventType, View.NO_ID, emitter); } From ee5455f0f03b5969d7370e6ed07e38dd8a8ce3f9 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Fri, 20 Oct 2017 10:38:56 +0200 Subject: [PATCH 24/48] Inline back retrieving RCTDeviceEmitter --- .../java/com/facebook/react/ReactRootView.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index f881a8712b37..fc1c5004a672 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -226,7 +226,9 @@ public boolean dispatchKeyEvent(KeyEvent ev) { "Unable to handle key event as the catalyst instance has not been attached"); return super.dispatchKeyEvent(ev); } - mAndroidTVRootViewHelper.handleKeyEvent(ev, getDeviceEventEmitter()); + ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + mAndroidTVRootViewHelper.handleKeyEvent(ev, emitter); return super.dispatchKeyEvent(ev); } @@ -240,7 +242,9 @@ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyF super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); return; } - mAndroidTVRootViewHelper.clearFocus(getDeviceEventEmitter()); + ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + mAndroidTVRootViewHelper.clearFocus(emitter); super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } @@ -254,13 +258,10 @@ public void requestChildFocus(View child, View focused) { super.requestChildFocus(child, focused); return; } - mAndroidTVRootViewHelper.onFocusChanged(focused, getDeviceEventEmitter()); - super.requestChildFocus(child, focused); - } - - private DeviceEventManagerModule.RCTDeviceEventEmitter getDeviceEventEmitter() { ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); - return reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + mAndroidTVRootViewHelper.onFocusChanged(focused, emitter); + super.requestChildFocus(child, focused); } private void dispatchJSTouchEvent(MotionEvent event) { From c76350676f43b940c6bc4f75e62298b1ef43f081 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Fri, 20 Oct 2017 11:01:19 +0200 Subject: [PATCH 25/48] Add Javadocs --- .../modules/systeminfo/AndroidInfoModule.java | 3 +++ .../ReactAndroidTVRootViewHelper.java | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) 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 f077730d1d99..65d7c03f6cf5 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 @@ -36,6 +36,9 @@ 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()) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java index d06ca3c1c115..2522972492be 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java @@ -12,14 +12,25 @@ import java.util.Arrays; import java.util.List; +/** + * Responsible for dispatching events specific for Android TV to support D-PAD navigation/selection. + * This is similar to AppleTV implementation and uses the same emitter events. + */ public class ReactAndroidTVRootViewHelper { + /** + * Android TV remote control sends a DPAD_CENTER event when clicking on a focused item. + * We add ENTER and SPACE to facilitate navigating on Android TV emulator. + */ private static final List PRESS_KEY_EVENTS = Arrays.asList( KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_SPACE ); + /** + * We keep reference to the last focused view id so that we can send a blur event when focus changes. + */ private int mLastFocusedViewId = View.NO_ID; private ReactRootView mReactRootView; @@ -28,6 +39,10 @@ public ReactAndroidTVRootViewHelper(ReactRootView reactRootView) { mReactRootView = reactRootView; } + /** + * Called from {@link ReactRootView}. + * This is the main place the Android TV remote key events are handled. + */ public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { int eventKeyCode = ev.getKeyCode(); int eventKeyAction = ev.getAction(); @@ -41,6 +56,9 @@ public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { } } + /** + * Called from {@link ReactRootView} when focused view changes. + */ public void onFocusChanged(View newFocusedView, RCTDeviceEventEmitter emitter) { if (mLastFocusedViewId == newFocusedView.getId()) { return; @@ -52,6 +70,9 @@ public void onFocusChanged(View newFocusedView, RCTDeviceEventEmitter emitter) { dispatchEvent("focus", newFocusedView.getId(), emitter); } + /** + * Called from {@link ReactRootView} when the whole view hierarchy looses focus. + */ public void clearFocus(RCTDeviceEventEmitter emitter) { if (mLastFocusedViewId != View.NO_ID) { dispatchEvent("blur", mLastFocusedViewId, emitter); From fb628ab62b9dc5d2d6fe14bdb13a6aff9c598ac9 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Fri, 20 Oct 2017 12:14:52 +0200 Subject: [PATCH 26/48] Add BuildingForAndroidTV docs --- docs/BuildingForAndroidTV.md | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/BuildingForAndroidTV.md diff --git a/docs/BuildingForAndroidTV.md b/docs/BuildingForAndroidTV.md new file mode 100644 index 000000000000..1fc757d5bc1f --- /dev/null +++ b/docs/BuildingForAndroidTV.md @@ -0,0 +1,96 @@ +--- +id: building-for-android-tv +title: Building For Android TV +layout: docs +category: Guides (Android) +permalink: docs/building-for-android-tv.html +banner: ejected +next: android-building-from-source +previous: signed-apk-android +--- + +## Build changes + +- *Native layer*: To run React Native project on Android TV make sure to make the following changes to `AndroidManifest.xml` + +```xml + + + ... + + ... + + + + ... + +``` + +- *JavaScript layer*: Support for Android TV has been added to `Platform.android.js`. You can check whether code is running on Android TV by doing + +```js +var Platform = require('Platform'); +var running_on_android_tv = Platform.isTV; +``` + +## Code changes + +- *Access to touchable controls*: When running on Android TV the Android framework will automatically apply a directional navigation scheme based on relative position of focusable elements in your views. The `Touchable` mixin has code added to detect focus changes and use existing methods to style the components properly and initiate the proper actions when the view is selected using the TV remote, so `TouchableHighlight`, `TouchableOpacity` and `TouchableNativeFeedback` will "just work". In particular: + + - `touchableHandleActivePressIn` will be executed when the touchable view goes into focus + - `touchableHandleActivePressOut` will be executed when the touchable view goes out of focus + - `touchableHandlePress` will be executed when the touchable view is actually selected by pressing the "select" button on the TV remote. + +- *TV remote/keyboard input*: A new native class, `ReactAndroidTVRootViewHelper`, sets up key events handlers for TV remote events. When TV remote events occur, this class fires a JS event. This event will be picked up by instances of the `TVEventHandler` JavaScript object. Application code that needs to implement custom handling of TV remote events can create an instance of `TVEventHandler` and listen for these events, as in the following code: + +```js +var TVEventHandler = require('TVEventHandler'); + +. +. +. + +class Game2048 extends React.Component { + _tvEventHandler: any; + + _enableTVEventHandler() { + this._tvEventHandler = new TVEventHandler(); + this._tvEventHandler.enable(this, function(cmp, evt) { + if (evt && evt.eventType === 'right') { + cmp.setState({board: cmp.state.board.move(2)}); + } else if(evt && evt.eventType === 'up') { + cmp.setState({board: cmp.state.board.move(1)}); + } else if(evt && evt.eventType === 'left') { + cmp.setState({board: cmp.state.board.move(0)}); + } else if(evt && evt.eventType === 'down') { + cmp.setState({board: cmp.state.board.move(3)}); + } else if(evt && evt.eventType === 'playPause') { + cmp.restartGame(); + } + }); + } + + _disableTVEventHandler() { + if (this._tvEventHandler) { + this._tvEventHandler.disable(); + delete this._tvEventHandler; + } + } + + componentDidMount() { + this._enableTVEventHandler(); + } + + componentWillUnmount() { + this._disableTVEventHandler(); + } + +``` + +- *Known issues*: + + - `InputText` components do not work for now (i.e. they cannot receive focus). + From 083c1b4c0c9be5a853ae0cf6cc8dadb640e964bd Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Fri, 20 Oct 2017 12:58:27 +0200 Subject: [PATCH 27/48] [WIP] Support opening dev menu by long playPause button press --- .../com/facebook/react/ReactActivity.java | 10 ++++++++++ .../facebook/react/ReactActivityDelegate.java | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java index 926b665e6535..3d82e2194ae8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java @@ -77,11 +77,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 6a0d47e8edc9..121bb8644e36 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -16,7 +16,6 @@ import com.facebook.react.devsupport.DoubleTapReloadRecognizer; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.PermissionListener; -import com.facebook.react.views.androidtv.ReactAndroidTVRootViewHelper; import javax.annotation.Nullable; @@ -133,6 +132,14 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } } + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { + event.startTracking(); + return true; + } + return false; + } + public boolean onKeyUp(int keyCode, KeyEvent event) { if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) { if (keyCode == KeyEvent.KEYCODE_MENU) { @@ -149,6 +156,16 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { return false; } + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { + getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); + return true; + } + } + return false; + } + public boolean onBackPressed() { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager().onBackPressed(); From 6b2a82c75da36d2c60e73474d4ae42c9264f2df1 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 23 Oct 2017 09:11:24 +0200 Subject: [PATCH 28/48] Add copyright header --- .../views/androidtv/ReactAndroidTVRootViewHelper.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java index 2522972492be..3212f1ecab37 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java @@ -1,3 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + package com.facebook.react.views.androidtv; import android.view.KeyEvent; From a7397bfb4c9bcac1f42a9ac87356c011cc9bb676 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 23 Oct 2017 10:35:58 +0200 Subject: [PATCH 29/48] Merge TV devices docs into one --- docs/BuildingForAndroidTV.md | 96 ---------- docs/BuildingForTV.md | 330 +++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+), 96 deletions(-) delete mode 100644 docs/BuildingForAndroidTV.md create mode 100644 docs/BuildingForTV.md diff --git a/docs/BuildingForAndroidTV.md b/docs/BuildingForAndroidTV.md deleted file mode 100644 index 1fc757d5bc1f..000000000000 --- a/docs/BuildingForAndroidTV.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -id: building-for-android-tv -title: Building For Android TV -layout: docs -category: Guides (Android) -permalink: docs/building-for-android-tv.html -banner: ejected -next: android-building-from-source -previous: signed-apk-android ---- - -## Build changes - -- *Native layer*: To run React Native project on Android TV make sure to make the following changes to `AndroidManifest.xml` - -```xml - - - ... - - ... - - - - ... - -``` - -- *JavaScript layer*: Support for Android TV has been added to `Platform.android.js`. You can check whether code is running on Android TV by doing - -```js -var Platform = require('Platform'); -var running_on_android_tv = Platform.isTV; -``` - -## Code changes - -- *Access to touchable controls*: When running on Android TV the Android framework will automatically apply a directional navigation scheme based on relative position of focusable elements in your views. The `Touchable` mixin has code added to detect focus changes and use existing methods to style the components properly and initiate the proper actions when the view is selected using the TV remote, so `TouchableHighlight`, `TouchableOpacity` and `TouchableNativeFeedback` will "just work". In particular: - - - `touchableHandleActivePressIn` will be executed when the touchable view goes into focus - - `touchableHandleActivePressOut` will be executed when the touchable view goes out of focus - - `touchableHandlePress` will be executed when the touchable view is actually selected by pressing the "select" button on the TV remote. - -- *TV remote/keyboard input*: A new native class, `ReactAndroidTVRootViewHelper`, sets up key events handlers for TV remote events. When TV remote events occur, this class fires a JS event. This event will be picked up by instances of the `TVEventHandler` JavaScript object. Application code that needs to implement custom handling of TV remote events can create an instance of `TVEventHandler` and listen for these events, as in the following code: - -```js -var TVEventHandler = require('TVEventHandler'); - -. -. -. - -class Game2048 extends React.Component { - _tvEventHandler: any; - - _enableTVEventHandler() { - this._tvEventHandler = new TVEventHandler(); - this._tvEventHandler.enable(this, function(cmp, evt) { - if (evt && evt.eventType === 'right') { - cmp.setState({board: cmp.state.board.move(2)}); - } else if(evt && evt.eventType === 'up') { - cmp.setState({board: cmp.state.board.move(1)}); - } else if(evt && evt.eventType === 'left') { - cmp.setState({board: cmp.state.board.move(0)}); - } else if(evt && evt.eventType === 'down') { - cmp.setState({board: cmp.state.board.move(3)}); - } else if(evt && evt.eventType === 'playPause') { - cmp.restartGame(); - } - }); - } - - _disableTVEventHandler() { - if (this._tvEventHandler) { - this._tvEventHandler.disable(); - delete this._tvEventHandler; - } - } - - componentDidMount() { - this._enableTVEventHandler(); - } - - componentWillUnmount() { - this._disableTVEventHandler(); - } - -``` - -- *Known issues*: - - - `InputText` components do not work for now (i.e. they cannot receive focus). - diff --git a/docs/BuildingForTV.md b/docs/BuildingForTV.md new file mode 100644 index 000000000000..e384c808b5ad --- /dev/null +++ b/docs/BuildingForTV.md @@ -0,0 +1,330 @@ +--- +id: building-for-tv +title: Building For TV Devices +layout: docs +category: Guides +permalink: docs/building-for-tv.html +banner: ejected +next: upgrading +previous: running-on-device +--- + + + +TV devices support has been implemented with the intention of making existing React Native applications "just work" on Apple TV and Android TV, with few or no changes needed in the JavaScript code for the applications. + +
+ +
    + + +
+
+ + + +The RNTester app supports Apple TV; use the `RNTester-tvOS` build target to build for tvOS. + +## Build changes + +- *Native layer*: React Native Xcode projects all now have Apple TV build targets, with names ending in the string '-tvOS'. + +- *react-native init*: New React Native projects created with `react-native init` will have Apple TV target automatically created in their XCode projects. + +- *JavaScript layer*: Support for Apple TV has been added to `Platform.ios.js`. You can check whether code is running on AppleTV by doing + +```js +var Platform = require('Platform'); +var running_on_tv = Platform.isTV; + +// If you want to be more specific and only detect devices running tvOS (but no Android TV devices) you can use: +var running_on_apple_tv = Platform.isTVOS +``` + + + +## Build changes + +- *Native layer*: To run React Native project on Android TV make sure to make the following changes to `AndroidManifest.xml` + +``` + + + ... + + ... + + + + ... + +``` + +- *JavaScript layer*: Support for Android TV has been added to `Platform.android.js`. You can check whether code is running on Android TV by doing + +```js +var Platform = require('Platform'); +var running_on_android_tv = Platform.isTV; +``` + + + +## Code changes + + + +- *General support for tvOS*: Apple TV specific changes in native code are all wrapped by the TARGET_OS_TV define. These include changes to suppress APIs that are not supported on tvOS (e.g. web views, sliders, switches, status bar, etc.), and changes to support user input from the TV remote or keyboard. + +- *Common codebase*: Since tvOS and iOS share most Objective-C and JavaScript code in common, most documentation for iOS applies equally to tvOS. + +- *Access to touchable controls*: When running on Apple TV, the native view class is `RCTTVView`, which has additional methods to make use of the tvOS focus engine. The `Touchable` mixin has code added to detect focus changes and use existing methods to style the components properly and initiate the proper actions when the view is selected using the TV remote, so `TouchableHighlight` and `TouchableOpacity` will "just work". In particular: + + - `touchableHandleActivePressIn` will be executed when the touchable view goes into focus + - `touchableHandleActivePressOut` will be executed when the touchable view goes out of focus + - `touchableHandlePress` will be executed when the touchable view is actually selected by pressing the "select" button on the TV remote. + + + +- *Access to touchable controls*: When running on Android TV the Android framework will automatically apply a directional navigation scheme based on relative position of focusable elements in your views. The `Touchable` mixin has code added to detect focus changes and use existing methods to style the components properly and initiate the proper actions when the view is selected using the TV remote, so `TouchableHighlight`, `TouchableOpacity` and `TouchableNativeFeedback` will "just work". In particular: + + - `touchableHandleActivePressIn` will be executed when the touchable view goes into focus + - `touchableHandleActivePressOut` will be executed when the touchable view goes out of focus + - `touchableHandlePress` will be executed when the touchable view is actually selected by pressing the "select" button on the TV remote. + + + +- *TV remote/keyboard input*: A new native class, `RCTTVRemoteHandler`, sets up gesture recognizers for TV remote events. When TV remote events occur, this class fires notifications that are picked up by `RCTTVNavigationEventEmitter` (a subclass of `RCTEventEmitter`), that fires a JS event. This event will be picked up by instances of the `TVEventHandler` JavaScript object. Application code that needs to implement custom handling of TV remote events can create an instance of `TVEventHandler` and listen for these events, as in the following code: + + + +- *TV remote/keyboard input*: A new native class, `ReactAndroidTVRootViewHelper`, sets up key events handlers for TV remote events. When TV remote events occur, this class fires a JS event. This event will be picked up by instances of the `TVEventHandler` JavaScript object. Application code that needs to implement custom handling of TV remote events can create an instance of `TVEventHandler` and listen for these events, as in the following code: + + + +```js +var TVEventHandler = require('TVEventHandler'); + +. +. +. + +class Game2048 extends React.Component { + _tvEventHandler: any; + + _enableTVEventHandler() { + this._tvEventHandler = new TVEventHandler(); + this._tvEventHandler.enable(this, function(cmp, evt) { + if (evt && evt.eventType === 'right') { + cmp.setState({board: cmp.state.board.move(2)}); + } else if(evt && evt.eventType === 'up') { + cmp.setState({board: cmp.state.board.move(1)}); + } else if(evt && evt.eventType === 'left') { + cmp.setState({board: cmp.state.board.move(0)}); + } else if(evt && evt.eventType === 'down') { + cmp.setState({board: cmp.state.board.move(3)}); + } else if(evt && evt.eventType === 'playPause') { + cmp.restartGame(); + } + }); + } + + _disableTVEventHandler() { + if (this._tvEventHandler) { + this._tvEventHandler.disable(); + delete this._tvEventHandler; + } + } + + componentDidMount() { + this._enableTVEventHandler(); + } + + componentWillUnmount() { + this._disableTVEventHandler(); + } + +``` + + + +- *Dev Menu support*: On the simulator, cmd-D will bring up the developer menu, just like on iOS. To bring it up on a real Apple TV device, make a long press on the play/pause button on the remote. (Please do not shake the Apple TV device, that will not work :) ) + +- *TV remote animations*: `RCTTVView` native code implements Apple-recommended parallax animations to help guide the eye as the user navigates through views. The animations can be disabled or adjusted with new optional view properties. + +- *Back navigation with the TV remote menu button*: The `BackHandler` component, originally written to support the Android back button, now also supports back navigation on the Apple TV using the menu button on the TV remote. + +- *TabBarIOS behavior*: The `TabBarIOS` component wraps the native `UITabBar` API, which works differently on Apple TV. To avoid jittery rerendering of the tab bar in tvOS (see [this issue](https://github.com/facebook/react-native/issues/15081)), the selected tab bar item can only be set from Javascript on initial render, and is controlled after that by the user through native code. + + + +- *Dev Menu support*: On the simulator, cmd-M will bring up the developer menu, just like on Android. To bring it up on a real Android TV device, make a long press on the play/pause button on the remote. (Please do not shake the Android TV device, that will not work :) ) + + + +- *Known issues*: + + - [ListView scrolling](https://github.com/facebook/react-native/issues/12793). The issue can be easily worked around by setting `removeClippedSubviews` to false in ListView and similar components. For more discussion of this issue, see [this PR](https://github.com/facebook/react-native/pull/12944). + + + +- *Known issues*: + + - `InputText` components do not work for now (i.e. they cannot receive focus). + + From 9404bff13e837d9f1e4e87f02da8cff3c8ba51e4 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 23 Oct 2017 11:43:51 +0200 Subject: [PATCH 30/48] Change dev menu trigger to fastForward instead of playPause --- .../main/java/com/facebook/react/ReactActivityDelegate.java | 4 ++-- docs/BuildingForTV.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 121bb8644e36..89b16ed460d0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -133,7 +133,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { event.startTracking(); return true; } @@ -158,7 +158,7 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyLongPress(int keyCode, KeyEvent event) { if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); return true; } diff --git a/docs/BuildingForTV.md b/docs/BuildingForTV.md index e384c808b5ad..5f8a1ed6ef8e 100644 --- a/docs/BuildingForTV.md +++ b/docs/BuildingForTV.md @@ -224,7 +224,7 @@ class Game2048 extends React.Component { -- *Dev Menu support*: On the simulator, cmd-M will bring up the developer menu, just like on Android. To bring it up on a real Android TV device, make a long press on the play/pause button on the remote. (Please do not shake the Android TV device, that will not work :) ) +- *Dev Menu support*: On the simulator, cmd-M will bring up the developer menu, just like on Android. To bring it up on a real Android TV device, make a long press on the fast forward button on the remote. (Please do not shake the Android TV device, that will not work :) ) From baaaf5a64dc58cc5b2b16ae1eb662a42c44bb3c4 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 23 Oct 2017 11:48:51 +0200 Subject: [PATCH 31/48] Add rewind/fastForward buttons support --- .../react/views/androidtv/ReactAndroidTVRootViewHelper.java | 4 ++++ docs/BuildingForTV.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java index 3212f1ecab37..1babb9d60631 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java @@ -59,6 +59,10 @@ public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { if (targetView != null) { if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { dispatchEvent("playPause", emitter); + } else if (KeyEvent.KEYCODE_MEDIA_REWIND == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { + dispatchEvent("rewind", emitter); + } else if (KeyEvent.KEYCODE_MEDIA_FAST_FORWARD == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { + dispatchEvent("fastForward", emitter); } else if (PRESS_KEY_EVENTS.contains(eventKeyCode) && eventKeyAction == KeyEvent.ACTION_UP) { dispatchEvent("select", targetView.getId(), emitter); } diff --git a/docs/BuildingForTV.md b/docs/BuildingForTV.md index 5f8a1ed6ef8e..1fd5e9977b59 100644 --- a/docs/BuildingForTV.md +++ b/docs/BuildingForTV.md @@ -212,6 +212,10 @@ class Game2048 extends React.Component { ``` + + +Additionally, `rewind` and `fastForward` events are sent after pressing the corresponding remote control button. + - *Dev Menu support*: On the simulator, cmd-D will bring up the developer menu, just like on iOS. To bring it up on a real Apple TV device, make a long press on the play/pause button on the remote. (Please do not shake the Apple TV device, that will not work :) ) From 4e4d603741ba2965ac1dd2c7b32650fca8c404eb Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 24 Oct 2017 13:20:13 +0200 Subject: [PATCH 32/48] Fix BUCK config --- ReactAndroid/src/main/java/com/facebook/react/BUCK | 1 + .../main/java/com/facebook/react/views/androidtv/BUCK | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/androidtv/BUCK diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK index 2fabe4a1196a..d37c42377805 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -32,6 +32,7 @@ rn_android_library( react_native_target("java/com/facebook/react/modules/systeminfo:systeminfo"), react_native_target("java/com/facebook/react/modules/toast:toast"), react_native_target("java/com/facebook/react/uimanager:uimanager"), + react_native_target("java/com/facebook/react/views/androidtv:androidtv"), react_native_target("java/com/facebook/react/views/imagehelper:imagehelper"), ], ) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/BUCK new file mode 100644 index 000000000000..53019df23454 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/BUCK @@ -0,0 +1,9 @@ +include_defs("//ReactAndroid/DEFS") + +android_library( + name = "androidtv", + srcs = glob(["*.java"]), + visibility = [ + "PUBLIC", + ], +) From a41b70d74420222569941ce7e5e7c6b47be8f02c Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 24 Oct 2017 14:16:02 +0200 Subject: [PATCH 33/48] Move ReactAndroidTVRootViewHelper to react/ package --- .../androidtv => }/ReactAndroidTVRootViewHelper.java | 3 +-- .../src/main/java/com/facebook/react/ReactRootView.java | 1 - .../main/java/com/facebook/react/views/androidtv/BUCK | 9 --------- 3 files changed, 1 insertion(+), 12 deletions(-) rename ReactAndroid/src/main/java/com/facebook/react/{views/androidtv => }/ReactAndroidTVRootViewHelper.java (98%) delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/androidtv/BUCK diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java similarity index 98% rename from ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java rename to ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java index 1babb9d60631..dd9772e1443c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java @@ -7,13 +7,12 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -package com.facebook.react.views.androidtv; +package com.facebook.react; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; -import com.facebook.react.ReactRootView; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index fc1c5004a672..1ea12ce1ec10 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -50,7 +50,6 @@ import com.facebook.react.uimanager.SizeMonitoringFrameLayout; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.events.EventDispatcher; -import com.facebook.react.views.androidtv.ReactAndroidTVRootViewHelper; import com.facebook.systrace.Systrace; import javax.annotation.Nullable; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/BUCK deleted file mode 100644 index 53019df23454..000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/views/androidtv/BUCK +++ /dev/null @@ -1,9 +0,0 @@ -include_defs("//ReactAndroid/DEFS") - -android_library( - name = "androidtv", - srcs = glob(["*.java"]), - visibility = [ - "PUBLIC", - ], -) From acf321d91309f615a96dc92682b44a51e1c3e331 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 24 Oct 2017 14:25:18 +0200 Subject: [PATCH 34/48] Fix BUCK config --- ReactAndroid/src/main/java/com/facebook/react/BUCK | 1 - 1 file changed, 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK index d37c42377805..2fabe4a1196a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -32,7 +32,6 @@ rn_android_library( react_native_target("java/com/facebook/react/modules/systeminfo:systeminfo"), react_native_target("java/com/facebook/react/modules/toast:toast"), react_native_target("java/com/facebook/react/uimanager:uimanager"), - react_native_target("java/com/facebook/react/views/androidtv:androidtv"), react_native_target("java/com/facebook/react/views/imagehelper:imagehelper"), ], ) From d2177d5f953235e9cbefa409fcec67b18339b47a Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 24 Oct 2017 15:23:28 +0200 Subject: [PATCH 35/48] Fix tests with AndroidInfoModule --- .../react/tests/CatalystNativeJSToJavaParametersTestCase.java | 2 +- .../com/facebook/react/tests/CatalystUIManagerTestCase.java | 2 +- .../java/com/facebook/react/tests/ProgressBarTestCase.java | 2 +- .../java/com/facebook/react/tests/ViewRenderingTestCase.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 174444f35b48..4f0131e29974 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java @@ -99,7 +99,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 c7de7b9ba020..5a1fb4bd507a 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java @@ -90,7 +90,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 fdc03beaa0dd..56dd5926cafa 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java @@ -81,7 +81,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 42e0c75f0817..13ffd362a108 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java @@ -62,7 +62,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()) From a087d057ecdb53d993cdf863fc81c86e2fc483de Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Thu, 9 Nov 2017 14:53:43 +0100 Subject: [PATCH 36/48] Add support for hasTVPreferredFocus on Android TV devices --- Libraries/Components/AppleTV/TVViewPropTypes.js | 10 +++------- Libraries/Components/Button.js | 7 +++++++ .../Touchable/TouchableNativeFeedback.android.js | 7 +++++++ Libraries/Components/Touchable/TouchableOpacity.js | 4 +--- .../View/PlatformViewPropTypes.android.js | 13 ------------- ...iewPropTypes.ios.js => PlatformViewPropTypes.js} | 2 +- .../facebook/react/views/view/ReactViewManager.java | 12 ++++++++++-- 7 files changed, 29 insertions(+), 26 deletions(-) delete mode 100644 Libraries/Components/View/PlatformViewPropTypes.android.js rename Libraries/Components/View/{PlatformViewPropTypes.ios.js => PlatformViewPropTypes.js} (95%) diff --git a/Libraries/Components/AppleTV/TVViewPropTypes.js b/Libraries/Components/AppleTV/TVViewPropTypes.js index b2c426d86241..092c828e20d0 100644 --- a/Libraries/Components/AppleTV/TVViewPropTypes.js +++ b/Libraries/Components/AppleTV/TVViewPropTypes.js @@ -17,17 +17,13 @@ var PropTypes = require('prop-types'); */ var 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 fc3f2ef5aab3..2f291f6934ad 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -55,6 +55,7 @@ class Button extends React.Component<{ title: string, onPress: () => any, color?: ?string, + hasTVPreferredFocus?: ?boolean, accessibilityLabel?: ?string, disabled?: ?boolean, testID?: ?string, @@ -77,6 +78,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 */ @@ -101,6 +106,7 @@ class Button extends React.Component<{ title, hasTVPreferredFocus, disabled, + hasTVPreferredFocus, testID, } = this.props; const buttonStyles = [styles.button]; @@ -132,6 +138,7 @@ class Button extends React.Component<{ hasTVPreferredFocus={hasTVPreferredFocus} testID={testID} disabled={disabled} + hasTVPreferredFocus={hasTVPreferredFocus} onPress={onPress}> {formattedTitle} diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js index bb20038f33c0..8d37a453e470 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -84,6 +84,11 @@ var 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 @@ -248,6 +253,8 @@ var 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 a27907ecef88..a694bfce8362 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -130,9 +130,7 @@ var 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 7d4734bdaa5f..000000000000 --- a/Libraries/Components/View/PlatformViewPropTypes.android.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule PlatformViewPropTypes - * @flow - */ - -module.export = {}; diff --git a/Libraries/Components/View/PlatformViewPropTypes.ios.js b/Libraries/Components/View/PlatformViewPropTypes.js similarity index 95% rename from Libraries/Components/View/PlatformViewPropTypes.ios.js rename to Libraries/Components/View/PlatformViewPropTypes.js index 7957c7b4a90e..cf3d6d02ba75 100644 --- a/Libraries/Components/View/PlatformViewPropTypes.ios.js +++ b/Libraries/Components/View/PlatformViewPropTypes.js @@ -13,7 +13,7 @@ const Platform = require('Platform'); var TVViewPropTypes = {}; -if (Platform.isTVOS) { +if (Platform.isTV) { TVViewPropTypes = require('TVViewPropTypes'); } 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 0ca79f814e07..8538a4d19409 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 @@ -58,8 +58,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, From 5dc25ab6a997614aa623bd5b5c96f677ff8018f4 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Fri, 8 Dec 2017 12:23:06 +0100 Subject: [PATCH 37/48] Remove obsolete docs --- docs/BuildingForTV.md | 334 ------------------------------------------ 1 file changed, 334 deletions(-) delete mode 100644 docs/BuildingForTV.md diff --git a/docs/BuildingForTV.md b/docs/BuildingForTV.md deleted file mode 100644 index 1fd5e9977b59..000000000000 --- a/docs/BuildingForTV.md +++ /dev/null @@ -1,334 +0,0 @@ ---- -id: building-for-tv -title: Building For TV Devices -layout: docs -category: Guides -permalink: docs/building-for-tv.html -banner: ejected -next: upgrading -previous: running-on-device ---- - - - -TV devices support has been implemented with the intention of making existing React Native applications "just work" on Apple TV and Android TV, with few or no changes needed in the JavaScript code for the applications. - -
- -
    - - -
-
- - - -The RNTester app supports Apple TV; use the `RNTester-tvOS` build target to build for tvOS. - -## Build changes - -- *Native layer*: React Native Xcode projects all now have Apple TV build targets, with names ending in the string '-tvOS'. - -- *react-native init*: New React Native projects created with `react-native init` will have Apple TV target automatically created in their XCode projects. - -- *JavaScript layer*: Support for Apple TV has been added to `Platform.ios.js`. You can check whether code is running on AppleTV by doing - -```js -var Platform = require('Platform'); -var running_on_tv = Platform.isTV; - -// If you want to be more specific and only detect devices running tvOS (but no Android TV devices) you can use: -var running_on_apple_tv = Platform.isTVOS -``` - - - -## Build changes - -- *Native layer*: To run React Native project on Android TV make sure to make the following changes to `AndroidManifest.xml` - -``` - - - ... - - ... - - - - ... - -``` - -- *JavaScript layer*: Support for Android TV has been added to `Platform.android.js`. You can check whether code is running on Android TV by doing - -```js -var Platform = require('Platform'); -var running_on_android_tv = Platform.isTV; -``` - - - -## Code changes - - - -- *General support for tvOS*: Apple TV specific changes in native code are all wrapped by the TARGET_OS_TV define. These include changes to suppress APIs that are not supported on tvOS (e.g. web views, sliders, switches, status bar, etc.), and changes to support user input from the TV remote or keyboard. - -- *Common codebase*: Since tvOS and iOS share most Objective-C and JavaScript code in common, most documentation for iOS applies equally to tvOS. - -- *Access to touchable controls*: When running on Apple TV, the native view class is `RCTTVView`, which has additional methods to make use of the tvOS focus engine. The `Touchable` mixin has code added to detect focus changes and use existing methods to style the components properly and initiate the proper actions when the view is selected using the TV remote, so `TouchableHighlight` and `TouchableOpacity` will "just work". In particular: - - - `touchableHandleActivePressIn` will be executed when the touchable view goes into focus - - `touchableHandleActivePressOut` will be executed when the touchable view goes out of focus - - `touchableHandlePress` will be executed when the touchable view is actually selected by pressing the "select" button on the TV remote. - - - -- *Access to touchable controls*: When running on Android TV the Android framework will automatically apply a directional navigation scheme based on relative position of focusable elements in your views. The `Touchable` mixin has code added to detect focus changes and use existing methods to style the components properly and initiate the proper actions when the view is selected using the TV remote, so `TouchableHighlight`, `TouchableOpacity` and `TouchableNativeFeedback` will "just work". In particular: - - - `touchableHandleActivePressIn` will be executed when the touchable view goes into focus - - `touchableHandleActivePressOut` will be executed when the touchable view goes out of focus - - `touchableHandlePress` will be executed when the touchable view is actually selected by pressing the "select" button on the TV remote. - - - -- *TV remote/keyboard input*: A new native class, `RCTTVRemoteHandler`, sets up gesture recognizers for TV remote events. When TV remote events occur, this class fires notifications that are picked up by `RCTTVNavigationEventEmitter` (a subclass of `RCTEventEmitter`), that fires a JS event. This event will be picked up by instances of the `TVEventHandler` JavaScript object. Application code that needs to implement custom handling of TV remote events can create an instance of `TVEventHandler` and listen for these events, as in the following code: - - - -- *TV remote/keyboard input*: A new native class, `ReactAndroidTVRootViewHelper`, sets up key events handlers for TV remote events. When TV remote events occur, this class fires a JS event. This event will be picked up by instances of the `TVEventHandler` JavaScript object. Application code that needs to implement custom handling of TV remote events can create an instance of `TVEventHandler` and listen for these events, as in the following code: - - - -```js -var TVEventHandler = require('TVEventHandler'); - -. -. -. - -class Game2048 extends React.Component { - _tvEventHandler: any; - - _enableTVEventHandler() { - this._tvEventHandler = new TVEventHandler(); - this._tvEventHandler.enable(this, function(cmp, evt) { - if (evt && evt.eventType === 'right') { - cmp.setState({board: cmp.state.board.move(2)}); - } else if(evt && evt.eventType === 'up') { - cmp.setState({board: cmp.state.board.move(1)}); - } else if(evt && evt.eventType === 'left') { - cmp.setState({board: cmp.state.board.move(0)}); - } else if(evt && evt.eventType === 'down') { - cmp.setState({board: cmp.state.board.move(3)}); - } else if(evt && evt.eventType === 'playPause') { - cmp.restartGame(); - } - }); - } - - _disableTVEventHandler() { - if (this._tvEventHandler) { - this._tvEventHandler.disable(); - delete this._tvEventHandler; - } - } - - componentDidMount() { - this._enableTVEventHandler(); - } - - componentWillUnmount() { - this._disableTVEventHandler(); - } - -``` - - - -Additionally, `rewind` and `fastForward` events are sent after pressing the corresponding remote control button. - - - -- *Dev Menu support*: On the simulator, cmd-D will bring up the developer menu, just like on iOS. To bring it up on a real Apple TV device, make a long press on the play/pause button on the remote. (Please do not shake the Apple TV device, that will not work :) ) - -- *TV remote animations*: `RCTTVView` native code implements Apple-recommended parallax animations to help guide the eye as the user navigates through views. The animations can be disabled or adjusted with new optional view properties. - -- *Back navigation with the TV remote menu button*: The `BackHandler` component, originally written to support the Android back button, now also supports back navigation on the Apple TV using the menu button on the TV remote. - -- *TabBarIOS behavior*: The `TabBarIOS` component wraps the native `UITabBar` API, which works differently on Apple TV. To avoid jittery rerendering of the tab bar in tvOS (see [this issue](https://github.com/facebook/react-native/issues/15081)), the selected tab bar item can only be set from Javascript on initial render, and is controlled after that by the user through native code. - - - -- *Dev Menu support*: On the simulator, cmd-M will bring up the developer menu, just like on Android. To bring it up on a real Android TV device, make a long press on the fast forward button on the remote. (Please do not shake the Android TV device, that will not work :) ) - - - -- *Known issues*: - - - [ListView scrolling](https://github.com/facebook/react-native/issues/12793). The issue can be easily worked around by setting `removeClippedSubviews` to false in ListView and similar components. For more discussion of this issue, see [this PR](https://github.com/facebook/react-native/pull/12944). - - - -- *Known issues*: - - - `InputText` components do not work for now (i.e. they cannot receive focus). - - From 3b709d2727e7386dd5366deacc7a05f313a5b836 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Fri, 8 Dec 2017 12:45:06 +0100 Subject: [PATCH 38/48] Fix post-rebase issues --- Libraries/Components/Button.js | 8 -------- .../java/com/facebook/react/ReactActivityDelegate.java | 5 ----- 2 files changed, 13 deletions(-) diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index 2f291f6934ad..ff2e9f138385 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -90,12 +90,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() { @@ -106,7 +100,6 @@ class Button extends React.Component<{ title, hasTVPreferredFocus, disabled, - hasTVPreferredFocus, testID, } = this.props; const buttonStyles = [styles.button]; @@ -138,7 +131,6 @@ class Button extends React.Component<{ hasTVPreferredFocus={hasTVPreferredFocus} testID={testID} disabled={disabled} - hasTVPreferredFocus={hasTVPreferredFocus} onPress={onPress}> {formattedTitle} diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 89b16ed460d0..de295799bd9f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -79,11 +79,6 @@ protected void onCreate(Bundle savedInstanceState) { mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); } - private boolean canHandleIntent(Intent intent) { - PackageManager packageManager = getContext().getPackageManager(); - return intent.resolveActivity(packageManager) != null; - } - protected void loadApp(String appKey) { if (mReactRootView != null) { throw new IllegalStateException("Cannot loadApp while app is already running."); From 5897b0e1ebc5131e4d09d89476f733898bc070a2 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Mon, 8 Jan 2018 09:03:46 +0100 Subject: [PATCH 39/48] Fix exporting TVViewPropTypes on Android --- Libraries/Components/View/PlatformViewPropTypes.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Libraries/Components/View/PlatformViewPropTypes.js b/Libraries/Components/View/PlatformViewPropTypes.js index cf3d6d02ba75..60194ccc492e 100644 --- a/Libraries/Components/View/PlatformViewPropTypes.js +++ b/Libraries/Components/View/PlatformViewPropTypes.js @@ -13,7 +13,10 @@ const Platform = require('Platform'); var TVViewPropTypes = {}; -if (Platform.isTV) { +// 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'); } From 0894fdb3b443bd13d8893a585d9ffded27c8ee1a Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Wed, 14 Feb 2018 08:37:38 +0100 Subject: [PATCH 40/48] Code review changes --- .../Components/AppleTV/TVEventHandler.js | 1 - Libraries/Utilities/Platform.ios.js | 9 +++++--- .../facebook/react/ReactActivityDelegate.java | 10 ++++---- .../react/ReactAndroidTVRootViewHelper.java | 23 ++++++++++--------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Libraries/Components/AppleTV/TVEventHandler.js b/Libraries/Components/AppleTV/TVEventHandler.js index 1a2a2edbd9e5..d5456367d77b 100644 --- a/Libraries/Components/AppleTV/TVEventHandler.js +++ b/Libraries/Components/AppleTV/TVEventHandler.js @@ -11,7 +11,6 @@ */ 'use strict'; -const React = require('React'); const Platform = require('Platform'); const TVNavigationEventEmitter = require('NativeModules').TVNavigationEventEmitter; const NativeEventEmitter = require('NativeEventEmitter'); diff --git a/Libraries/Utilities/Platform.ios.js b/Libraries/Utilities/Platform.ios.js index 8aeaa9ec90f8..bf6fa17102cc 100644 --- a/Libraries/Utilities/Platform.ios.js +++ b/Libraries/Utilities/Platform.ios.js @@ -24,12 +24,15 @@ const Platform = { const constants = NativeModules.PlatformConstants; return constants ? constants.interfaceIdiom === 'pad' : false; }, + /** + * Deprecated, use `isTV` instead. + */ get isTVOS() { - const constants = NativeModules.PlatformConstants; - return constants ? constants.interfaceIdiom === 'tv' : false; + return Platform.isTV; }, get isTV() { - return Platform.isTVOS; + const constants = NativeModules.PlatformConstants; + return constants ? constants.interfaceIdiom === 'tv' : false; }, get isTesting(): boolean { const constants = NativeModules.PlatformConstants; diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index de295799bd9f..500847f54469 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -152,11 +152,11 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { } public boolean onKeyLongPress(int keyCode, KeyEvent event) { - if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); - return true; - } + if (getReactNativeHost().hasInstance() && + getReactNativeHost().getUseDeveloperSupport() && + keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); + return true; } return false; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java index dd9772e1443c..deedc340eedf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java @@ -1,7 +1,7 @@ /** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. - * + *

* This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. @@ -55,16 +55,17 @@ public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { int eventKeyCode = ev.getKeyCode(); int eventKeyAction = ev.getAction(); View targetView = findFocusedView(mReactRootView); - if (targetView != null) { - if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { - dispatchEvent("playPause", emitter); - } else if (KeyEvent.KEYCODE_MEDIA_REWIND == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { - dispatchEvent("rewind", emitter); - } else if (KeyEvent.KEYCODE_MEDIA_FAST_FORWARD == eventKeyCode && eventKeyAction == KeyEvent.ACTION_UP) { - dispatchEvent("fastForward", emitter); - } else if (PRESS_KEY_EVENTS.contains(eventKeyCode) && eventKeyAction == KeyEvent.ACTION_UP) { - dispatchEvent("select", targetView.getId(), emitter); - } + if (targetView == null || eventKeyAction != KeyEvent.ACTION_UP) { + return; + } + if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode) { + dispatchEvent("playPause", emitter); + } else if (KeyEvent.KEYCODE_MEDIA_REWIND == eventKeyCode) { + dispatchEvent("rewind", emitter); + } else if (KeyEvent.KEYCODE_MEDIA_FAST_FORWARD == eventKeyCode) { + dispatchEvent("fastForward", emitter); + } else if (PRESS_KEY_EVENTS.contains(eventKeyCode)) { + dispatchEvent("select", targetView.getId(), emitter); } } From d6b12311e6e582d82b864ff529781289d6ae70a5 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Wed, 14 Feb 2018 08:52:22 +0100 Subject: [PATCH 41/48] Rename TVRootViewHelper to HWInputDeviceHelper --- ...ewHelper.java => ReactAndroidHWInputDeviceHelper.java} | 4 ++-- .../src/main/java/com/facebook/react/ReactRootView.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename ReactAndroid/src/main/java/com/facebook/react/{ReactAndroidTVRootViewHelper.java => ReactAndroidHWInputDeviceHelper.java} (97%) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java similarity index 97% rename from ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java rename to ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java index deedc340eedf..475548123024 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidTVRootViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java @@ -24,7 +24,7 @@ * Responsible for dispatching events specific for Android TV to support D-PAD navigation/selection. * This is similar to AppleTV implementation and uses the same emitter events. */ -public class ReactAndroidTVRootViewHelper { +public class ReactAndroidHWInputDeviceHelper { /** * Android TV remote control sends a DPAD_CENTER event when clicking on a focused item. @@ -43,7 +43,7 @@ public class ReactAndroidTVRootViewHelper { private ReactRootView mReactRootView; - public ReactAndroidTVRootViewHelper(ReactRootView reactRootView) { + public ReactAndroidHWInputDeviceHelper(ReactRootView reactRootView) { mReactRootView = reactRootView; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 1ea12ce1ec10..bd8515f5a59c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -87,7 +87,7 @@ public interface ReactRootViewEventListener { private boolean mIsAttachedToInstance; private boolean mShouldLogContentAppeared; private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this); - private final ReactAndroidTVRootViewHelper mAndroidTVRootViewHelper = new ReactAndroidTVRootViewHelper(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); @@ -227,7 +227,7 @@ public boolean dispatchKeyEvent(KeyEvent ev) { } ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); - mAndroidTVRootViewHelper.handleKeyEvent(ev, emitter); + mAndroidHWInputDeviceHelper.handleKeyEvent(ev, emitter); return super.dispatchKeyEvent(ev); } @@ -243,7 +243,7 @@ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyF } ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); - mAndroidTVRootViewHelper.clearFocus(emitter); + mAndroidHWInputDeviceHelper.clearFocus(emitter); super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } @@ -259,7 +259,7 @@ public void requestChildFocus(View child, View focused) { } ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); - mAndroidTVRootViewHelper.onFocusChanged(focused, emitter); + mAndroidHWInputDeviceHelper.onFocusChanged(focused, emitter); super.requestChildFocus(child, focused); } From 61ff2ceb8fb6e5c6cb18b85611025558c8f88b3b Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Wed, 14 Feb 2018 08:58:01 +0100 Subject: [PATCH 42/48] Improve handleKeyEvent method --- .../react/ReactAndroidHWInputDeviceHelper.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java index 475548123024..7bdd1af37a5d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java @@ -54,18 +54,21 @@ public ReactAndroidHWInputDeviceHelper(ReactRootView reactRootView) { public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { int eventKeyCode = ev.getKeyCode(); int eventKeyAction = ev.getAction(); - View targetView = findFocusedView(mReactRootView); - if (targetView == null || eventKeyAction != KeyEvent.ACTION_UP) { + if (eventKeyAction != KeyEvent.ACTION_UP) { return; } + if (PRESS_KEY_EVENTS.contains(eventKeyCode)) { + View targetView = findFocusedView(mReactRootView); + if (targetView != null) { + dispatchEvent("select", targetView.getId(), emitter); + } + } if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode) { dispatchEvent("playPause", emitter); } else if (KeyEvent.KEYCODE_MEDIA_REWIND == eventKeyCode) { dispatchEvent("rewind", emitter); } else if (KeyEvent.KEYCODE_MEDIA_FAST_FORWARD == eventKeyCode) { dispatchEvent("fastForward", emitter); - } else if (PRESS_KEY_EVENTS.contains(eventKeyCode)) { - dispatchEvent("select", targetView.getId(), emitter); } } From 8fc9b659421bdcc8a42d642a21c7318a9d783dfb Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Wed, 14 Feb 2018 12:24:11 +0100 Subject: [PATCH 43/48] Improve HWInputDeviceHelper events dispatching --- .../ReactAndroidHWInputDeviceHelper.java | 75 ++++++------------- .../com/facebook/react/ReactRootView.java | 2 +- 2 files changed, 23 insertions(+), 54 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java index 7bdd1af37a5d..58df894b47fd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java @@ -11,64 +11,54 @@ import android.view.KeyEvent; import android.view.View; -import android.view.ViewGroup; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.MapBuilder; import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; -import java.util.Arrays; -import java.util.List; +import java.util.Map; /** - * Responsible for dispatching events specific for Android TV to support D-PAD navigation/selection. - * This is similar to AppleTV implementation and uses the same emitter events. + * Responsible for dispatching events specific for hardware inputs. */ public class ReactAndroidHWInputDeviceHelper { /** - * Android TV remote control sends a DPAD_CENTER event when clicking on a focused item. - * We add ENTER and SPACE to facilitate navigating on Android TV emulator. + * Contains a mapping between handled KeyEvents and the corresponding navigation event + * that should be fired when the KeyEvent is received. */ - private static final List PRESS_KEY_EVENTS = Arrays.asList( + private static final Map KEY_EVENTS_ACTIONS = MapBuilder.of( KeyEvent.KEYCODE_DPAD_CENTER, + "select", KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_SPACE + "select", + KeyEvent.KEYCODE_SPACE, + "select", + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, + "playPause", + KeyEvent.KEYCODE_MEDIA_REWIND, + "rewind", + KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, + "fastForward" ); /** - * We keep reference to the last focused view id so that we can send a blur event when focus changes. + * 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 ReactRootView mReactRootView; - - public ReactAndroidHWInputDeviceHelper(ReactRootView reactRootView) { - mReactRootView = reactRootView; - } - /** * Called from {@link ReactRootView}. - * This is the main place the Android TV remote key events are handled. + * This is the main place the key events are handled. */ public void handleKeyEvent(KeyEvent ev, RCTDeviceEventEmitter emitter) { int eventKeyCode = ev.getKeyCode(); int eventKeyAction = ev.getAction(); - if (eventKeyAction != KeyEvent.ACTION_UP) { - return; - } - if (PRESS_KEY_EVENTS.contains(eventKeyCode)) { - View targetView = findFocusedView(mReactRootView); - if (targetView != null) { - dispatchEvent("select", targetView.getId(), emitter); - } - } - if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == eventKeyCode) { - dispatchEvent("playPause", emitter); - } else if (KeyEvent.KEYCODE_MEDIA_REWIND == eventKeyCode) { - dispatchEvent("rewind", emitter); - } else if (KeyEvent.KEYCODE_MEDIA_FAST_FORWARD == eventKeyCode) { - dispatchEvent("fastForward", emitter); + if (eventKeyAction == KeyEvent.ACTION_UP && KEY_EVENTS_ACTIONS.containsKey(eventKeyCode)) { + dispatchEvent(KEY_EVENTS_ACTIONS.get(eventKeyCode), mLastFocusedViewId, emitter); } } @@ -96,10 +86,6 @@ public void clearFocus(RCTDeviceEventEmitter emitter) { mLastFocusedViewId = View.NO_ID; } - private void dispatchEvent(String eventType, RCTDeviceEventEmitter emitter) { - dispatchEvent(eventType, View.NO_ID, emitter); - } - private void dispatchEvent(String eventType, int targetViewId, RCTDeviceEventEmitter emitter) { WritableMap event = new WritableNativeMap(); event.putString("eventType", eventType); @@ -108,21 +94,4 @@ private void dispatchEvent(String eventType, int targetViewId, RCTDeviceEventEmi } emitter.emit("onTVNavEvent", event); } - - private View findFocusedView(ViewGroup viewGroup) { - int childrenCount = viewGroup.getChildCount(); - for (int i = childrenCount - 1; i >= 0; i--) { - View view = viewGroup.getChildAt(i); - if (view.isFocused()) { - return view; - } - if (view instanceof ViewGroup) { - View nestedView = findFocusedView((ViewGroup) view); - if (nestedView != null) { - return nestedView; - } - } - } - return null; - } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index bd8515f5a59c..045d6d400dd6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -87,7 +87,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 final ReactAndroidHWInputDeviceHelper mAndroidHWInputDeviceHelper = new ReactAndroidHWInputDeviceHelper(); private boolean mWasMeasured = false; private int mWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); private int mHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); From dcb6ef7669f7e3c451ef219e675b920dbffaad3f Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Wed, 14 Feb 2018 12:53:11 +0100 Subject: [PATCH 44/48] Rename onTVNavEvent to onHWKeyEvent --- Libraries/Components/AppleTV/TVEventHandler.js | 2 +- React/Modules/RCTTVNavigationEventEmitter.m | 2 +- .../main/java/com/facebook/react/ReactActivityDelegate.java | 6 +++--- .../com/facebook/react/ReactAndroidHWInputDeviceHelper.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Libraries/Components/AppleTV/TVEventHandler.js b/Libraries/Components/AppleTV/TVEventHandler.js index d5456367d77b..299cb10bf86b 100644 --- a/Libraries/Components/AppleTV/TVEventHandler.js +++ b/Libraries/Components/AppleTV/TVEventHandler.js @@ -27,7 +27,7 @@ TVEventHandler.prototype.enable = function(component: ?any, callback: Function) this.__nativeTVNavigationEventEmitter = new NativeEventEmitter(TVNavigationEventEmitter); this.__nativeTVNavigationEventListener = this.__nativeTVNavigationEventEmitter.addListener( - 'onTVNavEvent', + 'onHWKeyEvent', (data) => { if (callback) { callback(component, data); diff --git a/React/Modules/RCTTVNavigationEventEmitter.m b/React/Modules/RCTTVNavigationEventEmitter.m index 12d177584011..2c143670cf75 100644 --- a/React/Modules/RCTTVNavigationEventEmitter.m +++ b/React/Modules/RCTTVNavigationEventEmitter.m @@ -11,7 +11,7 @@ NSString *const RCTTVNavigationEventNotification = @"RCTTVNavigationEventNotification"; -static NSString *const TVNavigationEventName = @"onTVNavEvent"; +static NSString *const TVNavigationEventName = @"onHWKeyEvent"; @implementation RCTTVNavigationEventEmitter diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 500847f54469..281cce05ed58 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -152,9 +152,9 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { } public boolean onKeyLongPress(int keyCode, KeyEvent event) { - if (getReactNativeHost().hasInstance() && - getReactNativeHost().getUseDeveloperSupport() && - keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + if (getReactNativeHost().hasInstance() + && getReactNativeHost().getUseDeveloperSupport() + && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); return true; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java index 58df894b47fd..b7e97723a674 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java @@ -92,6 +92,6 @@ private void dispatchEvent(String eventType, int targetViewId, RCTDeviceEventEmi if (targetViewId != View.NO_ID) { event.putInt("tag", targetViewId); } - emitter.emit("onTVNavEvent", event); + emitter.emit("onHWKeyEvent", event); } } From 1eccd4f722bb8d0d4db0482d87416d9ffec31c4f Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Thu, 15 Feb 2018 15:41:21 +0100 Subject: [PATCH 45/48] Add getUseDeveloperSupport check for fast forward long press --- .../main/java/com/facebook/react/ReactActivityDelegate.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 281cce05ed58..03141fb00b07 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -128,7 +128,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + if (getReactNativeHost().hasInstance() + && getReactNativeHost().getUseDeveloperSupport() + && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { event.startTracking(); return true; } From 3c4a8eb60e40fc316c2824d73ca92f5a8ae0b0e9 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Thu, 15 Feb 2018 16:35:00 +0100 Subject: [PATCH 46/48] Use ReactRootView sendEvent in HWInputDeviceHelper --- .../ReactAndroidHWInputDeviceHelper.java | 25 +++++++++++------- .../com/facebook/react/ReactRootView.java | 26 +++++++------------ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java index b7e97723a674..3e446cac3387 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java @@ -15,7 +15,6 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.MapBuilder; -import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; import java.util.Map; @@ -50,48 +49,54 @@ public class ReactAndroidHWInputDeviceHelper { */ 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, RCTDeviceEventEmitter emitter) { + 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, emitter); + dispatchEvent(KEY_EVENTS_ACTIONS.get(eventKeyCode), mLastFocusedViewId); } } /** * Called from {@link ReactRootView} when focused view changes. */ - public void onFocusChanged(View newFocusedView, RCTDeviceEventEmitter emitter) { + public void onFocusChanged(View newFocusedView) { if (mLastFocusedViewId == newFocusedView.getId()) { return; } if (mLastFocusedViewId != View.NO_ID) { - dispatchEvent("blur", mLastFocusedViewId, emitter); + dispatchEvent("blur", mLastFocusedViewId); } mLastFocusedViewId = newFocusedView.getId(); - dispatchEvent("focus", newFocusedView.getId(), emitter); + dispatchEvent("focus", newFocusedView.getId()); } /** * Called from {@link ReactRootView} when the whole view hierarchy looses focus. */ - public void clearFocus(RCTDeviceEventEmitter emitter) { + public void clearFocus() { if (mLastFocusedViewId != View.NO_ID) { - dispatchEvent("blur", mLastFocusedViewId, emitter); + dispatchEvent("blur", mLastFocusedViewId); } mLastFocusedViewId = View.NO_ID; } - private void dispatchEvent(String eventType, int targetViewId, RCTDeviceEventEmitter emitter) { + private void dispatchEvent(String eventType, int targetViewId) { WritableMap event = new WritableNativeMap(); event.putString("eventType", eventType); if (targetViewId != View.NO_ID) { event.putInt("tag", targetViewId); } - emitter.emit("onHWKeyEvent", event); + 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 045d6d400dd6..83ac70ebc98e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -87,7 +87,7 @@ public interface ReactRootViewEventListener { private boolean mIsAttachedToInstance; private boolean mShouldLogContentAppeared; private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this); - private final ReactAndroidHWInputDeviceHelper mAndroidHWInputDeviceHelper = new ReactAndroidHWInputDeviceHelper(); + 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); @@ -225,9 +225,7 @@ public boolean dispatchKeyEvent(KeyEvent ev) { "Unable to handle key event as the catalyst instance has not been attached"); return super.dispatchKeyEvent(ev); } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); - DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); - mAndroidHWInputDeviceHelper.handleKeyEvent(ev, emitter); + mAndroidHWInputDeviceHelper.handleKeyEvent(ev); return super.dispatchKeyEvent(ev); } @@ -241,9 +239,7 @@ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyF super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); return; } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); - DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); - mAndroidHWInputDeviceHelper.clearFocus(emitter); + mAndroidHWInputDeviceHelper.clearFocus(); super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } @@ -257,9 +253,7 @@ public void requestChildFocus(View child, View focused) { super.requestChildFocus(child, focused); return; } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); - DeviceEventManagerModule.RCTDeviceEventEmitter emitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); - mAndroidHWInputDeviceHelper.onFocusChanged(focused, emitter); + mAndroidHWInputDeviceHelper.onFocusChanged(focused); super.requestChildFocus(child, focused); } @@ -707,13 +701,13 @@ 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); - } + void sendEvent(String eventName, @Nullable WritableMap params) { + if (mReactInstanceManager != null) { + mReactInstanceManager.getCurrentReactContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); } } } From 15bf2874f2bf283db9cd803514dcdf8347478c11 Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Thu, 15 Feb 2018 16:43:26 +0100 Subject: [PATCH 47/48] Add "package" to sendEvent method --- .../java/com/facebook/react/ReactRootView.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 83ac70ebc98e..581dde3f4ae2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -572,6 +572,14 @@ public void handleException(Throwable t) { 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; @@ -702,12 +710,4 @@ private void emitUpdateDimensionsEvent() { .emitUpdateDimensionsEvent(); } } - - void sendEvent(String eventName, @Nullable WritableMap params) { - if (mReactInstanceManager != null) { - mReactInstanceManager.getCurrentReactContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(eventName, params); - } - } } From 9a6be6d4ee9e38aa08001a0b1d9f5d503ba1188e Mon Sep 17 00:00:00 2001 From: Krzysztof Ciombor Date: Tue, 6 Mar 2018 11:03:05 +0100 Subject: [PATCH 48/48] Fix ReactAndroidHWInputDeviceHelper.java license --- .../facebook/react/ReactAndroidHWInputDeviceHelper.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java index 3e446cac3387..8c3df938340e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactAndroidHWInputDeviceHelper.java @@ -1,10 +1,8 @@ /** * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - *

- * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * + * 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;