Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Android TV devices #16500

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
0e40e4f
Add LEANBACK_LAUNCHER to RNTester AndroidManifest
Oct 16, 2017
6dc6bb5
Add ReactAndroidTVViewManager
Oct 16, 2017
8a71318
Add isRunningOnTv flag to AndroidInfoModule constants
Oct 16, 2017
7788de1
Expose isTV flag in Platform constants
Oct 16, 2017
29eb058
Copy iOS TVEventHandler implementation over to Android
Oct 16, 2017
bf86743
Change TV check in Touchable component
Oct 16, 2017
2eac232
Use ReactAndroidTVViewManager as default RootView
Oct 16, 2017
b961f2d
Prevent TouchableNativeFeedback from crashing on Android TV
Oct 16, 2017
51df826
Prevent crash when asking for overlay permission on Android TV
Oct 16, 2017
d8c0f64
Decouple ReactAvtivityDelegate from ReactAndroidTVRootViewHelper
Oct 16, 2017
4a75d81
Add comment to RNTester Manifest for leanback launcher
Oct 16, 2017
5e7a6cd
Change isRunningOnTV to uiMode
Oct 17, 2017
c4218f6
Change condition check to dispatch hotspot update
Oct 17, 2017
a5a7b3b
Return true after dispatchKeyEvent
Oct 17, 2017
beb738f
Fix isTV Platform constants on iOS
Oct 17, 2017
ac9d584
Consolidate TVEventHandler to single file
Oct 17, 2017
318218f
Remove mReactContext local variable from AndroidInfoModule
Oct 17, 2017
2c37223
Minor renamed in ReactAndroidTVRootViewHelper
Oct 17, 2017
4aeca35
Prevent sending select event if target component is disabled
Oct 17, 2017
9e04243
Revert "Return true after dispatchKeyEvent"
Oct 17, 2017
1a68b10
Handle focus change events in ReactRootView
Oct 19, 2017
8ea9950
Add launcher banner icon
Oct 20, 2017
db8cf86
Inline keyEvent handler methods
Oct 20, 2017
ee5455f
Inline back retrieving RCTDeviceEmitter
Oct 20, 2017
c763506
Add Javadocs
Oct 20, 2017
fb628ab
Add BuildingForAndroidTV docs
Oct 20, 2017
083c1b4
[WIP] Support opening dev menu by long playPause button press
Oct 20, 2017
6b2a82c
Add copyright header
Oct 23, 2017
a7397bf
Merge TV devices docs into one
Oct 23, 2017
9404bff
Change dev menu trigger to fastForward instead of playPause
Oct 23, 2017
baaaf5a
Add rewind/fastForward buttons support
Oct 23, 2017
4e4d603
Fix BUCK config
Oct 24, 2017
a41b70d
Move ReactAndroidTVRootViewHelper to react/ package
Oct 24, 2017
acf321d
Fix BUCK config
Oct 24, 2017
d2177d5
Fix tests with AndroidInfoModule
Oct 24, 2017
a087d05
Add support for hasTVPreferredFocus on Android TV devices
Nov 9, 2017
5dc25ab
Remove obsolete docs
Dec 8, 2017
3b709d2
Fix post-rebase issues
Dec 8, 2017
5897b0e
Fix exporting TVViewPropTypes on Android
Jan 8, 2018
0894fdb
Code review changes
Feb 14, 2018
d6b1231
Rename TVRootViewHelper to HWInputDeviceHelper
Feb 14, 2018
61ff2ce
Improve handleKeyEvent method
Feb 14, 2018
8fc9b65
Improve HWInputDeviceHelper events dispatching
Feb 14, 2018
dcb6ef7
Rename onTVNavEvent to onHWKeyEvent
Feb 14, 2018
1eccd4f
Add getUseDeveloperSupport check for fast forward long press
Feb 15, 2018
3c4a8eb
Use ReactRootView sendEvent in HWInputDeviceHelper
Feb 15, 2018
15bf287
Add "package" to sendEvent method
Feb 15, 2018
e79041a
Merge branch 'master' into feature/AndroidTV
Mar 6, 2018
9a6be6d
Fix ReactAndroidHWInputDeviceHelper.java license
Mar 6, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions Libraries/Components/AppleTV/TVEventHandler.android.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/
'use strict';

const React = require('React');
const Platform = require('Platform');
const TVNavigationEventEmitter = require('NativeModules').TVNavigationEventEmitter;
const NativeEventEmitter = require('NativeEventEmitter');

Expand All @@ -19,13 +19,13 @@ function TVEventHandler() {
}

TVEventHandler.prototype.enable = function(component: ?any, callback: Function) {
if (!TVNavigationEventEmitter) {
if (Platform.OS === 'ios' && !TVNavigationEventEmitter) {
return;
}

this.__nativeTVNavigationEventEmitter = new NativeEventEmitter(TVNavigationEventEmitter);
this.__nativeTVNavigationEventListener = this.__nativeTVNavigationEventEmitter.addListener(
'onTVNavEvent',
'onHWKeyEvent',
(data) => {
if (callback) {
callback(component, data);
Expand Down
10 changes: 3 additions & 7 deletions Libraries/Components/AppleTV/TVViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,13 @@ const PropTypes = require('prop-types');
*/
const TVViewPropTypes = {
/**
* *(Apple TV only)* When set to true, this view will be focusable
* and navigable using the Apple TV remote.
*
* @platform ios
* When set to true, this view will be focusable
* and navigable using the TV remote.
*/
isTVSelectable: PropTypes.bool,

/**
* *(Apple TV only)* May be set to true to force the Apple TV focus engine to move focus to this view.
*
* @platform ios
* May be set to true to force the TV focus engine to move focus to this view.
*/
hasTVPreferredFocus: PropTypes.bool,

Expand Down
11 changes: 5 additions & 6 deletions Libraries/Components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Button extends React.Component<{
title: string,
onPress: () => any,
color?: ?string,
hasTVPreferredFocus?: ?boolean,
accessibilityLabel?: ?string,
disabled?: ?boolean,
testID?: ?string,
Expand All @@ -75,6 +76,10 @@ class Button extends React.Component<{
* If true, disable all interactions for this component.
*/
disabled: PropTypes.bool,
/**
* TV preferred focus (see documentation for the View component).
*/
hasTVPreferredFocus: PropTypes.bool,
/**
* Handler to be called when the user taps the button
*/
Expand All @@ -83,12 +88,6 @@ class Button extends React.Component<{
* Used to locate this view in end-to-end tests.
*/
testID: PropTypes.string,
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
*
* @platform ios
*/
hasTVPreferredFocus: PropTypes.bool,
};

render() {
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Components/Touchable/Touchable.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ const LONG_PRESS_ALLOWED_MOVEMENT = 10;
*/
const TouchableMixin = {
componentDidMount: function() {
if (!Platform.isTVOS) {
if (!Platform.isTV) {
return;
}

Expand All @@ -329,7 +329,7 @@ const TouchableMixin = {
} else if (evt.eventType === 'blur') {
cmp.touchableHandleActivePressOut && cmp.touchableHandleActivePressOut(evt);
} else if (evt.eventType === 'select') {
cmp.touchableHandlePress && cmp.touchableHandlePress(evt);
cmp.touchableHandlePress && !cmp.props.disabled && cmp.touchableHandlePress(evt);
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ const TouchableNativeFeedback = createReactClass({
*/
background: backgroundPropType,

/**
* TV preferred focus (see documentation for the View component).
*/
hasTVPreferredFocus: PropTypes.bool,

/**
* Set to true to add the ripple effect to the foreground of the view, instead of the
* background. This is useful if one of your child views has a background of its own, or you're
Expand Down Expand Up @@ -156,7 +161,9 @@ const TouchableNativeFeedback = createReactClass({
touchableHandleActivePressIn: function(e: Event) {
this.props.onPressIn && this.props.onPressIn(e);
this._dispatchPressedStateChange(true);
this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY);
if (this.pressInLocation) {
this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY);
}
},

touchableHandleActivePressOut: function(e: Event) {
Expand Down Expand Up @@ -244,6 +251,8 @@ const TouchableNativeFeedback = createReactClass({
testID: this.props.testID,
onLayout: this.props.onLayout,
hitSlop: this.props.hitSlop,
isTVSelectable: true,
hasTVPreferredFocus: this.props.hasTVPreferredFocus,
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
onResponderGrant: this.touchableHandleResponderGrant,
Expand Down
4 changes: 1 addition & 3 deletions Libraries/Components/Touchable/TouchableOpacity.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@ const TouchableOpacity = createReactClass({
*/
activeOpacity: PropTypes.number,
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
*
* @platform ios
* TV preferred focus (see documentation for the View component).
*/
hasTVPreferredFocus: PropTypes.bool,
/**
Expand Down
11 changes: 0 additions & 11 deletions Libraries/Components/View/PlatformViewPropTypes.android.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
const Platform = require('Platform');

let TVViewPropTypes = {};
if (Platform.isTVOS) {
// We need to always include TVViewPropTypes on Android
// as unlike on iOS we can't detect TV devices at build time
// and hence make view manager export a different list of native properties.
if (Platform.isTV || Platform.OS === 'android') {
TVViewPropTypes = require('TVViewPropTypes');
}

Expand Down
4 changes: 4 additions & 0 deletions Libraries/Utilities/Platform.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const Platform = {
const constants = NativeModules.PlatformConstants;
return constants && constants.isTesting;
},
get isTV(): boolean {
const constants = NativeModules.PlatformConstants;
return constants && constants.uiMode === 'tv';
},
select: (obj: Object) => 'android' in obj ? obj.android : obj.default,
};

Expand Down
6 changes: 6 additions & 0 deletions Libraries/Utilities/Platform.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ const Platform = {
const constants = NativeModules.PlatformConstants;
return constants ? constants.interfaceIdiom === 'pad' : false;
},
/**
* Deprecated, use `isTV` instead.
*/
get isTVOS() {
return Platform.isTV;
},
get isTV() {
const constants = NativeModules.PlatformConstants;
return constants ? constants.interfaceIdiom === 'tv' : false;
},
Expand Down
7 changes: 7 additions & 0 deletions RNTester/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@
android:minSdkVersion="16"
android:targetSdkVersion="23" />

<!--
android:icon is used to display launcher icon on mobile devices.
android:banner is used to display a rectangular banned launcher icon on Android TV devices.
-->
<application
android:name=".RNTesterApplication"
android:allowBackup="true"
android:banner="@drawable/tv_banner"
android:icon="@drawable/launcher_icon"
android:label="@string/app_name"
android:theme="@style/Theme.ReactNative.AppCompat.Light" >
Expand All @@ -34,6 +39,8 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- Needed to properly create a launch intent when running on Android TV -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion React/Modules/RCTTVNavigationEventEmitter.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

NSString *const RCTTVNavigationEventNotification = @"RCTTVNavigationEventNotification";

static NSString *const TVNavigationEventName = @"onTVNavEvent";
static NSString *const TVNavigationEventName = @"onHWKeyEvent";

@implementation RCTTVNavigationEventEmitter

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public void run() {
mRecordingTestModule = new RecordingTestModule();
mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this)
.addNativeModule(mRecordingTestModule)
.addNativeModule(new AndroidInfoModule())
.addNativeModule(new AndroidInfoModule(getContext()))
.addNativeModule(new DeviceInfoModule(getContext()))
.addNativeModule(new AppStateModule(getContext()))
.addNativeModule(new FakeWebSocketModule())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public void run() {

jsModule = ReactTestHelper.catalystInstanceBuilder(this)
.addNativeModule(uiManager)
.addNativeModule(new AndroidInfoModule())
.addNativeModule(new AndroidInfoModule(getContext()))
.addNativeModule(new DeviceInfoModule(getContext()))
.addNativeModule(new AppStateModule(getContext()))
.addNativeModule(new FakeWebSocketModule())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public void run() {

mInstance = ReactTestHelper.catalystInstanceBuilder(this)
.addNativeModule(mUIManager)
.addNativeModule(new AndroidInfoModule())
.addNativeModule(new AndroidInfoModule(getContext()))
.addNativeModule(new DeviceInfoModule(getContext()))
.addNativeModule(new AppStateModule(getContext()))
.addNativeModule(new FakeWebSocketModule())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void run() {

mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this)
.addNativeModule(uiManager)
.addNativeModule(new AndroidInfoModule())
.addNativeModule(new AndroidInfoModule(getContext()))
.addNativeModule(new DeviceInfoModule(getContext()))
.addNativeModule(new AppStateModule(getContext()))
.addNativeModule(new FakeWebSocketModule())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public List<ModuleSpec> getNativeModules(final ReactApplicationContext reactCont
new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new AndroidInfoModule();
return new AndroidInfoModule(reactContext);
}
}),
ModuleSpec.nativeModuleSpec(
Expand Down
10 changes: 10 additions & 0 deletions ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,21 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
mDelegate.onActivityResult(requestCode, resultCode, data);
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}

@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return mDelegate.onKeyLongPress(keyCode, event) || super.onKeyLongPress(keyCode, event);
}

@Override
public void onBackPressed() {
if (!mDelegate.onBackPressed()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
}

public boolean onKeyDown(int keyCode, KeyEvent event) {
if (getReactNativeHost().hasInstance()
&& getReactNativeHost().getUseDeveloperSupport()
&& keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
event.startTracking();
return true;
}
return false;
}

public boolean onKeyUp(int keyCode, KeyEvent event) {
if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
Expand All @@ -143,6 +153,16 @@ public boolean onKeyUp(int keyCode, KeyEvent event) {
return false;
}

public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (getReactNativeHost().hasInstance()
&& getReactNativeHost().getUseDeveloperSupport()
&& keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
return true;
}
return false;
}

public boolean onBackPressed() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onBackPressed();
Expand Down
Loading