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
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.
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();
});
}
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: