This codelab will demonstrate seamless hop of the location service among a mobile phone, automotive head unit, and wearable. A user searches for a destination on the mobile phone and starts navigation. After getting on the car, the user can tap the Migrate button on the mobile phone to hop the navigation to the automotive head unit. Before getting off the car, the user can tap the Migrate button on the head unit to seamlessly hop the navigation to both the watch and mobile phone. Then the watch will help navigate the user to walk to the destination in the last mile. The following figure shows the scenario.

The procedure is as follows:

You can use the following devices to complete operations in this codelab:
A mobile phone and a tablet running HarmonyOS with the developer mode enabled
A wearable running HarmonyOS and paired with the mobile phone via the Huawei Health app

This codelab focuses only on core code. You can obtain the full code according to 10 References. The following figure shows the code structure of the project.

You need to apply for the required permissions before developing the hop capability. For details about application permissions, see Available Permissions.
Permission for accessing the Internet
ohos.permission.INTERNET
Permission for obtaining the location
ohos.permission.LOCATION
Permissions related to distributed data management
ohos.permission.DISTRIBUTED_DATASYNC
ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE
ohos.permission.GET_DISTRIBUTED_DEVICE_INFO
ohos.permission.GET_BUNDLE_INFO

Map-related APIs

The map and navigation data used in this codelab is obtained through Amap APIs. For details, see the Amap official website for developers. The following constants are defined in the Const file:

Map Loading Component

You can load a map by loading the map tile data of Amap. The screen display on the mobile phone is divided into tiles based on the configured tile size. Each tile is used to load a map tile. All tiles are combined to form a complete map. The following steps demonstrate how to encapsulate the map loading capability into the custom component NavMap:
Step 1 - Implement the map component.
Use initMapCanvas to obtain data including the number of tile rows (columns) and the real tile length.

public void initMapCanvas(boolean isRefresh) { // Number of rows (columns) of tiles at a specified zoom level int rowCount = (int) Math.pow(COMPONENT_HALF, ZOOM); tileRealLength = OVER_LENGTH * COMPONENT_HALF / rowCount; mapComponentWidth = ScreenUtils.getScreenWidth(getContext()); mapComponentHeight = ScreenUtils.getScreenHeight(getContext()); double minX = centerPoint.getPointX() - mapComponentWidth / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH; colMin = (int) Math.floor((minX + OVER_LENGTH) / tileRealLength); double maxX = centerPoint.getPointX() + mapComponentWidth / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH; colMax = Math.min((int) Math.floor((maxX + OVER_LENGTH) / tileRealLength), rowCount - 1); double maxY = centerPoint.getPointY() + mapComponentHeight / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH; rowMin = Math.min(rowCount - 1 - (int) Math.floor((maxY + OVER_LENGTH) / tileRealLength), rowCount - 1); double minY = centerPoint.getPointY() - mapComponentHeight / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH; rowMax = rowCount - 1 - (int) Math.floor((minY + OVER_LENGTH) / tileRealLength); addDrawTask(this); setTouchEventListener(this); initTiles(isRefresh); initElement(isRefresh); }

Call the initTiles method to request the Amap API to obtain tiles, and call the setTiles method to set data for mapTiles.

private void initTiles(boolean isRefresh) { if (mapTiles == null || isRefresh) { mapTiles = new CopyOnWriteArrayList<>(); } mapTiles.removeIf(mapTile -> !mapTile.isInBoundary(rowMin, rowMax, colMin, colMax)); getContext().getGlobalTaskDispatcher(TaskPriority.DEFAULT).asyncDispatch(this::setTiles); }

Call drawTiles in the onDraw() method of NavMap to draw map tiles.

private void drawTiles(Canvas canvas) { for (MapTile mapTile : mapTiles) { canvas.drawPixelMapHolder(mapTile, mapTile.getNowPointX(), mapTile.getNowPointY(), linePaint); } }

Step 2 - Obtain the current location.
To load the map of the area where the user is located, your app needs to obtain the location information (longitude and latitude) of the device first. The methods for obtaining the current location information are encapsulated in LocationHelper, and then the longitude and latitude are passed to the Amap API to obtain the city code. The sample code is as follows:

public void getMyLocation() { new LocationHelper().getMyLocation(context, loc -> { double locLongitude = loc.getLongitude(); double locLatitude = loc.getLatitude(); location = locLongitude + "," + locLatitude; if (navMap.getMapElements() == null) { setMapCenter(locLongitude, locLatitude); getRegionDetail(); } }); }

Step 3 - Load the map.
After obtaining the longitude and latitude, call the setCenterPoint method in MapElement to load the map.

public void setMapCenter(double lon, double lat) { double[] mercators = MapUtils.lonLat2Mercator(lon, lat); Point centerPoint = new Point((float) mercators[0], (float) mercators[1]); navMap.setCenterPoint(centerPoint); MapElement peopleEle = new MapElement(centerPoint.getPointX(), centerPoint.getPointY(), true); peopleEle.setActionType(Const.ROUTE_PEOPLE); navMap.addElement(peopleEle); }

—-End

Setting the Start and End Positions

Call the Amap geocoding API to obtain the localCityCode of the city where the device is located.

private void getRegionDetail() { String url = String.format(Const.REGION_DETAIL_URL, location, Const.MAP_KAY); HttpUtils.getInstance(context).get(url, result -> { RegionDetailResult regionDetailResult = GsonUtils.jsonToBean(result, RegionDetailResult.class); localCityCode = regionDetailResult.getRegeocode().getAddressComponent().getCitycode(); }); }

After the user types a keyword in the input box for the start or end position, call the getInputTips API to obtain the coordinates, which are displayed in a ListContainer component. Then, the user can select the start or end position from the list.

public void getInputTips(String keyWords) { String url = String.format(Const.INPUT_TIPS_URL, keyWords, Const.MAP_KAY, location, localCityCode); HttpUtils.getInstance(context).get(url, result -> { InputTipsResult inputTipsResult = GsonUtils.jsonToBean(result, InputTipsResult.class); if (inputTipsResult == null) { return; } dataCallBack.setInputTipsView(inputTipsResult.getTips()); }); }

Obtaining the Route Plan

After obtaining the start and end coordinates, call getRouteResult to obtain the route planning result. Then, call the MapHelper.parseRoute method to parse the route planning result, and add the created MapElement object to the elements of the TinyMap component.

public void getRouteResult(String endLocation) { String url = String.format(Const.ROUTE_URL, location, endLocation, Const.MAP_KAY); HttpUtils.getInstance(context).get(url, result -> dataCallBack.setRouteView(result)); }

Drawing the Navigation Route

In the preceding section, you have obtained the elements object that is a collection of elements on the planned route. You can invoke the HarmonyOS Path class to connect the elements in the elements object into a route. Then onDraw is called to draw the route.

public void onDraw(Component component, Canvas canvas) { path.reset(); grayPath.reset(); drawTiles(canvas); drawRoutePath(canvas); drawImageElement(canvas); } private void drawRoutePath(Canvas canvas) { for (int i = 1; i < elements.size(); i++) { MapElement mapElement = elements.get(i); Point point = mapElement.getNowPoint(); float pointX = point.getPointX(); float pointY = point.getPointY(); if (i == 1 || i == elements.size() - 1) { path.moveTo(pointX, pointY); } else { path.lineTo(pointX, pointY); } if (i <= stepPoint) { if (i == 1 || i == elements.size() - 1) { grayPath.moveTo(pointX, pointY); } else { grayPath.lineTo(pointX, pointY); } } } // Draw the navigation route that has not been traveled. canvas.drawPath(path, paint); // Draw the navigation route that has been traveled. canvas.drawPath(grayPath, grayPaint); }

Simulating the Track Movement

Movement on the navigation route actually refers to the movement of the location icon from the coordinates of one element in the navigation route to the coordinates of the next element.
In MainAbilitySlice, add a listener for starting the navigation upon a click event.

@Override public void onClick(Component component) { switch (component.getId()) { // Start navigation. case ResourceTable.Id_start_nav: mapManager.startNav(); setStartNavView(); break; } }

In MapManager, use EventHandler to deliver a Runnable task to the thread's event queue and execute the task when it comes out of the queue, thereby implementing continuous movement of the location icon. The implementation code is as follows:

public void startNav() { if (!mapEventHandler.hasInnerEvent(task)) { mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE); connectWatch(); } } private Runnable task = new Runnable() { @Override public void run() { // Assign the coordinates of the next element on the navigation route to the location icon. MapElement peopleElement = navMap.getMapElements().get(0); nextElement = navMap.getMapElements().get(stepPoint + 1); peopleElement.setMercatorPoint(nextElement.getMercatorPoint()); peopleElement.setNowPoint(nextElement.getNowPoint()); peopleElement.setOriginPoint(nextElement.getOriginPoint()); // Call the sendEvent method to send a UI update event with a specified priority to the event queue. mapEventHandler.sendEvent(1, EventHandler.Priority.IMMEDIATE); // Send a delayed Runnable task with a specified priority to the event queue so that the task can be executed continuously, thereby implementing continuous movement of the location icon. mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE); stepPoint++; // Send the stepPoint to NavMap. The stepPoint is also used when you draw a route that has been traveled. navMap.setStepPoint(stepPoint); LogUtils.info(TAG, "run......" + stepPoint); if (stepPoint >= navMap.getMapElements().size() - 1) { mapEventHandler.removeTask(task); } } };

Call the invalidate() method of NavMap to re-draw the map and implement track movement.

private class MapEventHandler extends EventHandler { private MapEventHandler(EventRunner runner) { super(runner); } @Override public void processEvent(InnerEvent event) { ... navMap.invalidate(); } }

Data Hop from a Mobile Phone to a Wearable

Data is hopped from a mobile phone to a wearable through the IDL. For details about the IDL, see Defining HarmonyOS IDL Interfaces. After a user starts navigation on the mobile phone, the WatchService of the wearable is connected and the WatchAbility is started. The implementation code is as follows:

// Add a listener for starting navigation upon a click event in MainAbilitySlice. @Override public void onClick(Component component) { switch (component.getId()) { // Start navigation. case ResourceTable.Id_start_nav: mapManager.startNav(); setStartNavView(); break; } } // Connect to WatchService of the wearable in MapManager and start WatchAbility at the same time. public void startNav() { if (!mapEventHandler.hasInnerEvent(task)) { mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE); connectWatch(); } }

In the processEvent method of the MapEventHandler, call requestRemote to send data to the WatchService of the wearable.

private class MapEventHandler extends EventHandler { private MapEventHandler(EventRunner runner) { super(runner); } @Override public void processEvent(InnerEvent event) { super.processEvent(event); if (event.eventId != 1) { return; } LogUtils.info(TAG, "processEvent invalidate"); if (nextElement.getActionType() != null && !nextElement.getActionType().isEmpty()) { navListener.onNavListener(nextElement); } if (proxy != null) { requestRemote(nextElement.getActionType() == null ? "" : nextElement.getActionType(), nextElement.getActionContent() == null ? "" : nextElement.getActionContent()); } navMap.invalidate(); } }

After receiving the data, WatchService sends the data to WatchAbility by publishing a common event.

public class WatchRemote extends MapIdlInterfaceStub { private WatchRemote(String descriptor) { super(descriptor); } @Override public void action(String actionType, String actionContent) throws RemoteException { LogUtils.info(TAG, "WatchService::action"); sendEvent(actionType, actionContent); } } private void sendEvent(String actionType, String actionContent) { LogUtils.info(TAG, "WatchService::sendEvent"); try { Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder().withAction("com.huawei.map").build(); intent.setOperation(operation); intent.setParam("actionType", actionType); intent.setParam("actionContent", actionContent); CommonEventData eventData = new CommonEventData(intent); CommonEventManager.publishCommonEvent(eventData); } catch (RemoteException e) { LogUtils.info(TAG, "publishCommonEvent occur exception."); } }

WatchAbilitySlice receives data sent by WatchService and processes the data.

private class MyCommonEventSubscriber extends CommonEventSubscriber { MyCommonEventSubscriber(CommonEventSubscribeInfo info) { super(info); } @Override public void onReceiveEvent(CommonEventData commonEventData) { Intent intent = commonEventData.getIntent(); String actionType = intent.getStringParam("actionType"); String actionContent = intent.getStringParam("actionContent"); if (actionType != null) { if (actionType.equals(Const.STOP_WATCH_ABILITY)) { terminateAbility(); } else { actionImg.setPixelMap(ImageUtils.getImageId(actionType)); contentComponent.setText(actionContent); } } } }

Data Hop Between a Tablet and a Mobile Phone

Data hop between a mobile phone and a tablet is implemented by continueAbility. Both MainAbility and MainAbilitySlice need to implement the IAbilityContinuation interface. For details about how to use continueAbility, see Hop Overview. The following uses data hop from a mobile phone to a tablet as an example.
When a user taps the Migrate button on the mobile phone, the continueAbility method and then the onSaveData method are called on the phone. At the same time, the MainAbilitySlice is started on the tablet, and it then calls onRestoreData to restore the hopped data on the tablet. The specific implementation is in MainAbilitySlice.
The onSaveData method is called on the mobile phone to save the data to be hopped to the MainAbilitySlice of the tablet.

@Override public boolean onSaveData(IntentParams saveData) { String elementsString = GsonUtils.objectToString(navMap.getMapElements()); saveData.setParam(ELEMENT_STRING, elementsString); saveData.setParam("stepPoint", mapManager.getStepPoint()); LogUtils.info(TAG, "onSaveData" + navMap.getMapElements().size()); return true; }

The MainAbilitySlice on the tablet fetches data from the onRestoreData method.

@Override public boolean onRestoreData(IntentParams restoreData) { if (restoreData.getParam(ELEMENT_STRING) instanceof String) { String elementsString = (String) restoreData.getParam(ELEMENT_STRING); elements = GsonUtils.jsonToList(elementsString, MapElement.class); } stepPoint = (int) restoreData.getParam("stepPoint"); LogUtils.info(TAG, "onRestoreData::elements::" + elements.size()); return true; }

After obtaining the data, the tablet uses the data in the initView() method to draw a map.
If the user taps the Migrate button on the tablet, the data will be hopped from the tablet to the mobile phone in the same way.

The following figures show the navigation hop effects among a mobile phone, wearable, and tablet.

Well done. You have completed this codelab and learned:

Demo code on gitee

Code copied