Disconnect on Inactivity for Roku Apps

Roku with You.i Roku Cloud apps are almost inactive while the Roku client is playing a video, or when the end user isn’t using the Roku remote. During this time, Cloud server instances can be made available to other Roku clients by saving their state, which can be used by the new server instances to resume the application. This allows a greater number of users to be serviced by the Cloud infrastructure while reducing costs and the number of server instances running at a given time. We call this feature “Disconnect on Inactivity”.

To support this feature in your app, you need to add app logic to save and restore the app’s state at the appropriate times.

How Disconnect on Inactivity Works

When a Roku client is inactive for more than the noActivitySessionTimeoutSec time, the You.i Roku Cloud infrastructure disconnects the Roku client from the Cloud server by providing it with the saved state information from the previous connection. The client reconnects again with the server after the end-user’s interaction with the app.

The Cloud module’s screenSaverWillStart event, invoked 30 seconds before the screensaver starts, triggers the You.i Roku Cloud app to store its state on the Roku client. The You.i Roku Cloud server saves its state with the Cloud.saveInstanceState(savedState) API for a You.i React Native app, in response to the screenSaverWillStart event. The Roku server instance is destroyed once the saveInstanceState is complete.

The saved state is a deep link to the screen at the point when the client became inactive. A stack of screen data could also be stored to restore the history. The app state is stored by the Roku client as a JSON stringified blob.

Sequence of Events Between Client, Infrastructure, and Server

The following sequence diagram illustrates the sequence of events between the Roku client, Cloud infrastructure, and Cloud server when the screensaver is active for longer than noActivitySessionTimeoutSec.

Sequence diagram of a scenario when the screensaver is active for longer than noActivitySessionTimeoutSec

The following sequence diagram illustrates the sequence of events between the Roku client, Cloud infrastructure, and Cloud server when the screensaver is active for less than noActivitySessionTimeoutSec, but the server exceeds two ping durations (which is every 10 seconds) from the client.

Sequence diagram of a scenario when the screensaver is active for less than noActivitySessionTimeoutSec

Server Activity Cycle for Disconnect on Inactivity

The following diagram explains the You.i Roku Cloud server activity cycle for Disconnect on Inactivity:

You.i Roku Cloud server activity cycle for disconnect on inactivity

Enabling Disconnect on Inactivity

To enable the Disconnect on Inactivity feature, you need to update the client configuration file and set up a persistent key for the app state data.

Edit the Client Configuration File

Add the following to clientConfig.json, located at ../youi/build/<platform>/<configuration>/assets/json/default/:

{
  "inactivityDisconnect": true
}

Setting inactivityDisconnect to true causes the application to save its state when the client is inactive.

The client sends the screenSaverWillStart event 30 seconds before the screensaver actually starts, which enables the server to send the app state data to the client. If the screensaver active time is longer than noActivitySessionTimeoutSec, which is configured by the Cloud infrastructure, the connection to the server instance is terminated. On an end-user’s interaction with the app, the connection between client and new server instance is established, and the client starts from the saved state, instead of the beginning.

Set Up a Persistent Key for the App State Data

The steps for setting up a persistent key for the app state data depend on whether you’re using React Navigation or React Redux.

Using React Navigation

This is an example from RNSampleApp, where NavigateState is specified as the persistent key for the app state.

C++ setup for React Navigation 3.x
bool App::UserInit()
{
    // ...
    #if YI_CLOUD_SERVER
        GetReactNativeViewController().AddModule<CloudConfig>();
    #endif
 // ...
}

bool App::UserStart()
{
    bool isOK = PlatformApp::UserStart();
    #if YI_CLOUD_SERVER
        CYICloud::GetInterface().SetNavigationPersistenceKey("NavigationState");
    #endif
    return isOK;
}
React Native application component for React Navigation 3.x
// up to react-navigation version 3.x
class YiReactApp extends React.Component {
  render() {
    return <RootStack persistenceKey={"NavigationState"}/>;
  }
}
React Native application component for React Navigation 4.x or later
// for react navigation version 4.x or above
class App extends PureComponent {
  navigationRef = React.createRef();

  constructor(props) {
    super(props);
    cloudEventEmitter.addListener('clientBackgrounded', this._saveInstanceState);
    cloudEventEmitter.addListener('screenSaverWillStart', this._saveInstanceState);
  }

  _saveInstanceState = () => {
    Cloud.saveInstanceState({ navigation: this.navigationRef.current.state });
  };

  _onLoadNavigationState = () => {
    return Cloud.savedInstanceState && Cloud.savedInstanceState.length > 0 && Cloud.savedInstanceState[0].navigation;
  };

  render() {
    return <NavigationContainer ref={this.navigationRef} loadNavigationState={this._onLoadNavigationState} />;
  }
}

Using React Redux

Redux-based RN apps need to register to receive the Cloud module’s clientBackgrounded or screenSaverWillStart changes depending on the Disconnect on Inactivity configuration. When the app state changes to clientBackgrounded or screenSaverWillStart, to save its state, the app needs to call the Cloud module’s saveInstanceState with the folly::dynamic object parameter that includes its current and screen history stack.

The Cloud module constant savedInstanceState must be checked when the application is being launched. If this state is present, it is decoded with the JSON parser to recreate the current screen and screen history portion of its Redux state.

Restoring the UI Information

To be able to restore the application properly, the application needs to store a variety of UI-specific information for each screen, such as current focus and list scroll positions.

Commonly, this UI-specific information is already stored as part of the app’s state when working with Redux or React Navigation. If it’s not, the app needs to add additional logic to persist this information. The data for these values could be stored as follows:

  • Current focus: an app-generated ID that is associated with a particular focusable element

    <Button onFocus={() => {/* Update focus state for screen */ } />
    
  • List scroll position: a key-value pair that matches a list’s app-generated ID with an index

    ({index}) => <Button onFocus={() => {/* Update scrollIndex for list */ />}
    

The application’s performance can be impacted depending on how you save the data. As this information might not be needed until the application is restored, consider a strategy that does not re-render the application unnecessarily. One option is to store disconnect-specific information in a map, which can be accessed by each component through a React Context.

When restoring the application, lists can restore their position using the key-value mapping to set the lists initialScrollIndex, as contentOffset is currently unsupported with You.i React Native. Download the zip file to see the reference implementation of the persistent module that is used to restore the UI information.