Nowadays, TVs are controlled mainly through their own remote controls, which provide limited functions. For example, if you want to search for programs on the TV, you can enter only letters or numbers with the help of the remote control. The distributed remote control corporates the input capability of mobile phones with the remote control capability of TV remote controls, so you can quickly and conveniently control your TV.
The distributed remote control is implemented based on the HarmonyOS Distributed Scheduler and Common Event Service (CES). When the search box on the TV is focused, the input app on your mobile phone automatically starts. When you enter content on the mobile phone, the content is displayed in the search box on the TV. After you touch OK, the TV searches for programs based on the content. You can also touch arrow keys (Up, Down, Left, and Right) to move the focus to a desired program and touch OK to play the program.

Figure 1 Default home screen of the TV

Figure 2 Dialog box for selecting a mobile phone (After you select a mobile phone, the input app on that mobile phone automatically starts.)

Figure 3 Input app screen on the mobile phone

Figure 4 Content entered on the mobile phone displayed on the TV synchronously (You can touch to focus a searched result.)

Figure 5 Touching to play the selected program

You can use the following devices to run the code in this codelab:

This codelab illustrates only the core code. You can download the complete code by referring to 10 Sample Code. The following figure shows the code structure of the entire project.

To develop this app, you must apply for the following permissions related to multi-device collaboration. For details about how to apply for app permissions, see Available Permissions.

  1. ohos.permission.DISTRIBUTED_DATASYNC: Allows an app to exchange user data with another device.
  2. ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE: Allows an app to monitor device status changes in distributed networking.
  3. ohos.permission.GET_DISTRIBUTED_DEVICE_INFO: Allows an app to obtain the device list and device information in distributed networking.
  4. ohos.permission.GET_BUNDLE_INFO: Allows a non-system app to query information about other apps.

This topic describes the layout of each screen to help you understand the mapping between screens and layouts.

The dialog box for selecting a device (shown in Figure 2) is dynamically created based on the code and layout file. The layout files aredialog_select_device.xmll anditem_device_list.xmll inresources\base\layoutt. Thedialog_select_device.xmll file defines the layout of the entire dialog box and the title bar of the dialog box, and theitem_device_list.xmll file defines the layout of the device list in the dialog box. You can view the layout files to learn about the content. When the device selection dialog box is displayed, the app code queries available devices and updates the content displayed in the dialog box. The code snippet is as follows:

private CommonDialog commonDialog; List<DeviceInfo> deviceInfos = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE); initView(context, deviceIfs, callBack);
private void initView(Context context, List devices, SelectResultListener listener) { commonDialog = new CommonDialog(context); commonDialog.setAlignment(LayoutAlignment.CENTER); Component dialogLayout = LayoutScatter.getInstance(context) .parse(ResourceTable.Layout_dialog_select_device, null, false); commonDialog.setSize(WIDTH, HEIGHT); commonDialog.setAutoClosable(true); commonDialog.setContentCustomComponent(dialogLayout); if (dialogLayout.findComponentById(ResourceTable.Id_list_devices) instanceof ListContainer) { ListContainer devicesListContainer = (ListContainer) dialogLayout .findComponentById(ResourceTable.Id_list_devices); DevicesListAdapter devicesListAdapter = new DevicesListAdapter(devices, context); devicesListContainer.setItemProvider(devicesListAdapter); devicesListContainer.setItemClickedListener((listContainer, component, position, ll) -> { listener.callBack(devices.get(position)); commonDialog.hide(); }); } dialogLayout.findComponentById(ResourceTable.Id_cancel).setClickedListener(component -> { commonDialog.hide(); }); }
  • For the input app screen on a mobile phone (shown in Figure 3), the layout file is ability_remote_input.xml in resources\base\layout. The layout file uses DirectionalLayout and DependentLayout to lay out TextField, Image, and Text components. You can view the layout files to learn about the content.
  • For the program playback screen (shown in Figure 5), the layout files are ability_movie_play.xml and simple_player_controller_layout.xml in resources\base\layout. The ability_movie_play.xml file defines the overall layout of the program playback screen. You can view the file content to understand the implementation. When a program is played, add a drawing surface to the playback screen. The code snippet is as follows:
    SurfaceProvider surfaceView = new SurfaceProvider(this); DependentLayout.LayoutConfig layoutConfig = new DependentLayout.LayoutConfig(); layoutConfig.addRule(DependentLayout.LayoutConfig.CENTER_IN_PARENT); surfaceView.setLayoutConfig(layoutConfig); surfaceView.setVisibility(Component.VISIBLE); surfaceView.setFocusable(Component.FOCUS_ENABLE); surfaceView.setTouchFocusable(true); surfaceView.requestFocus(); surfaceView.pinToZTop(false); surfaceView.getSurfaceOps().get().addCallback(mSurfaceCallback); if (findComponentById(ResourceTable.Id_parent_layout) instanceof DependentLayout) { DependentLayout dependentLayout = (DependentLayout) findComponentById(ResourceTable.Id_parent_layout); SimplePlayerController simplePlayerController = new SimplePlayerController(this, player); dependentLayout.addComponent(surfaceView); dependentLayout.addComponent(simplePlayerController); }

    The simple_player_controller_layout.xml file defines the layout of the control bar on the playback screen. You can view the file content to understand the implementation. The code snippet for initializing the control bar and switching the play/pause icon on the playback screen is as follows:

    private void initView() { Component playerController = LayoutScatter.getInstance(mContext) .parse(ResourceTable.Layout_simple_player_controller_layout, null, false); addComponent(playerController); if (findComponentById(ResourceTable.Id_play_controller) instanceof Image) { playToggle = (Image) findComponentById(ResourceTable.Id_play_controller); } } private void initListener() { playToggle.setClickedListener(component -> { if (controllerPlayer.isNowPlaying()) { controllerPlayer.pause(); playToggle.setPixelMap(ResourceTable.Media_video_play); } else { controllerPlayer.play(); playToggle.setPixelMap(ResourceTable.Media_video_stop); } }); }

    After you click the search box on the TV home screen, a dialog box is displayed (shown in Figure 2), asking you to select a mobile phone. After you select a mobile phone, the corresponding mobile phone immediately starts the input app screen. The core code for starting the input app screen on the mobile phone is as follows:

    public void openRemoteAbility(String deviceId, String bundleName, String abilityName) { Intent intent = new Intent(); String localDeviceId = KvManagerFactory.getInstance() .createKvManager(new KvManagerConfig(abilitySlice)).getLocalDeviceInfo().getId(); intent.setParam("localDeviceId", localDeviceId); Operation operation = new Intent.OperationBuilder() .withDeviceId(deviceId) .withBundleName(bundleName) .withAbilityName(abilityName) .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); intent.setOperation(operation); abilitySlice.startAbility(intent); }

    In the preceding code, localDeviceId indicates the TV's device ID, which is used as the parameter for starting the mobile phone FA and is passed to the remote control UI for pairing with the corresponding TV. When starting the remote control FA, use the initConnManager() method to connect to RemoteService on the TV. The core code for connecting to RemoteService is as follows:

    public void connectPa(Context context, String deviceId) { if (deviceId != null && !deviceId.trim().isEmpty()) { Intent connectPaIntent = new Intent(); Operation operation = new Intent.OperationBuilder() .withDeviceId(deviceId) .withBundleName(context.getBundleName()) .withAbilityName(RemoteService.class.getName()) .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); connectPaIntent.setOperation(operation); conn = new IAbilityConnection() { @Override public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) { LogUtils.info(TAG, "===connectRemoteAbility done"); proxy = new MyRemoteProxy(remote); } @Override public void onAbilityDisconnectDone(ElementName elementName, int resultCode) { LogUtils.info(TAG, "onAbilityDisconnectDone......"); proxy = null; } }; context.connectAbility(connectPaIntent, conn); } }

    After the connection is set up, you can perform input, search, and movement operations by using the corresponding buttons on the remote control UI.

    Related click events are bound when the input app on the mobile phone is started and the screen is initialized. The input app on the mobile phone can perform the following control operations: Up, Down, Left, Right, OK, Back, and Close.

    private void initListener() { // Listen to text changes and perform remote synchronization. textField.addTextObserver((ss, ii, i1, i2) -> { Map<String, String> map = new HashMap<>(INIT_SIZE); map.put("inputString", ss); connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_DATA, map); }); okButton.setClickedListener(component -> { // Listen to a click event on the OK button. buttonClickSound(); String searchString = textField.getText(); Map<String, String> map = new HashMap<>(INIT_SIZE); map.put("inputString", searchString); connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_SEARCH, map); }); leftButton.setClickedListener(component -> { // Listen to a click event on the Left button. sendMoveRequest(Constants.MOVE_LEFT); }); rightButton.setClickedListener(component -> { // Listen to a click event on the Right button. sendMoveRequest(Constants.MOVE_RIGHT); }); upButton.setClickedListener(component -> { // Listen to a click event on the Up button. sendMoveRequest(Constants.MOVE_UP); }); downButton.setClickedListener(component -> { // Listen to a click event on the Down button. sendMoveRequest(Constants.MOVE_DOWN); }); goBackButton.setClickedListener(component -> { // Listen to a click event on the Back button. sendMoveRequest(Constants.GO_BACK); }); closeButton.setClickedListener(component -> { // Listen to a click event on the Close button. sendMoveRequest(Constants.GO_BACK); terminateAbility(); }); }

    Send the operation request from the remote mobile phone to the TV using the distributed task scheduling capability and common event management capability.

    private void sendMoveRequest(String direction) { buttonClickSound(); Map<String, String> map = new HashMap<>(INIT_SIZE); map.put("move", direction); connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_MOVE, map); }

    The TV receives the common events and processes them. If the received event is text input, the input content is filled in the TV search box. If the received event is a movement, the focus moves to the specified position. If the received event is OK, the TV executes the search logic when the focus is on the search box and plays the program when the focus is on the program component. If the received event is Back, the TV home screen is displayed, and the focus is moved to the search box.

    class MyCommonEventSubscriber extends CommonEventSubscriber { MyCommonEventSubscriber(CommonEventSubscribeInfo info) { super(info); } @Override public void onReceiveEvent(CommonEventData commonEventData) { Intent intent = commonEventData.getIntent(); int requestType = intent.getIntParam("requestType", 0); String inputString = intent.getStringParam("inputString"); if (requestType == ConnectManagerIml.REQUEST_SEND_DATA) { tvTextInput.setText(inputString); } else if (requestType == ConnectManagerIml.REQUEST_SEND_SEARCH) { if (componentPointDataNow.getPointX() == 0) { // Call the search method of the TV. searchMovies(tvTextInput.getText()); return; } // Call the TV method to play a movie. abilityMgr.playMovie(getBundleName(), MOVIE_PLAY_ABILITY); } else { // Call the TV method to move the focus. String moveString = intent.getStringParam("move"); MainCallBack.movePoint(MainAbilitySlice.this, moveString); } } }

    Implement the focus moving method. To prevent concurrency due to consecutive touches on the arrow keys, add a lock to this method. Clear the focus effect of the previous focus and set the focus effect of the new focus.

    public void move(int pointX, int pointY) { MOVE_LOCK.lock(); try { // Set the focus movement. if (pointX == 0 && componentPointDataNow.getPointX() > 0) { scrollView.fluentScrollByY(pointY * size.height); } if (componentPointDataNow.getPointX() == 0 && pointX == 1) { scrollView.scrollTo(0, 0); } // Set the background. if (componentPointDataNow.getPointX() + pointX == 0) { setBackGround(componentPointDataNow.getPointX() + pointX, 1); } else { setBackGround(componentPointDataNow.getPointX() + pointX, componentPointDataNow.getPointY() + pointY); } } finally { MOVE_LOCK.unlock(); } }

    Implement the search method. Clear the previous search result and background effect, and set a new search result and background effect.

    private void searchMovies(String text) { if (text == null || "".equals(text)) { return; } // Clear the last search result and background effect. clearHistroyBackGround(); for (ComponentPointData componentPointData : ComponentPointDataMgr.getComponentPointDataMgrs()) { if (MovieSearchUtils.isContainMovie(componentPointData.getMovieName(), text) || MovieSearchUtils.isContainMovie(componentPointData.getMovieFirstName(), text)) { movieSearchList.add(componentPointData); Component component = findComponentById(componentPointData.getComponentId()); component.setPadding(SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING); } } if (movieSearchList.size() > 0) { componentPointDataNow = movieSearchList.get(0); Component component = findComponentById(componentPointDataNow.getComponentId()); component.setPadding(FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING); } else { Component component = findComponentById(componentPointDataNow.getComponentId()); component.requestFocus(); } }

    This codelab introduces the device connection and remote control of the distributed remote control. It intuitively shows the distributed feature of HarmonyOS. Through this codelab, you've learnt how to start distributed apps, how to receive and send common events, and how to implement distributed task scheduling. You've also learnt the UI layout and video playback functions.
    During distributed app development, you need to learn and master cross-device collaboration, distributed task scheduling, and common event notification of HarmonyOS.

    Well done! You have completed this codelab and learned:

    • Usage of Common Layouts
    • Distributed Scheduler
    • Common Events and Notifications

    Source code on Gitee

    Source code on GitHub

    Code copied