如何配置后台持续定位长时任务
==============

#### 问题现象

实时导航等场景下，应用可能需要退至后台并持续更新位置信息，如何实现后台持续定位长时任务？

#### 背景知识

*   [长时任务(ArkTS)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/continuous-task)：应用退至后台后，在后台需要长时间运行用户可感知的任务，如播放音乐、导航等。为防止应用进程被挂起，导致对应功能异常，可以申请长时任务，使应用在后台长时间运行。
*   [Location Kit（位置服务）](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/location-kit)：位置子系统使用多种定位技术提供服务，如GNSS定位、基站定位、WLAN/蓝牙定位（基站定位、WLAN/蓝牙定位后续统称“网络定位技术”）；通过这些定位技术，无论用户设备在室内或是户外，都可以准确地确定设备位置。
*   [geoLocationManager.on('locationChange')](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-geolocationmanager#geolocationmanageronlocationchange)：获取实时位置的接口，开启位置变化订阅，并发起定位请求。

#### 解决方案

1.  在module.json5文件的requestPermissions中声明位置权限和后台运行位置权限。
    
    {
      "name" : "ohos.permission.LOCATION",
      "usedScene": {
        "abilities": \[
          "EntryAbility"
        \],
        "when": "inuse"
      },
      "reason": "$string:location\_use"
    },
    {
      "name" : "ohos.permission.APPROXIMATELY\_LOCATION",
      "usedScene": {
        "abilities": \[
          "EntryAbility"
        \],
        "when": "inuse"
      },
      "reason": "$string:location\_use"
    },
    {
      "name" : "ohos.permission.LOCATION\_IN\_BACKGROUND",
      "usedScene": {
        "abilities": \[
          "EntryAbility"
        \],
        "when": "inuse"
      },
      "reason": "$string:location\_use"
    },
    {
      "name" : "ohos.permission.KEEP\_BACKGROUND\_RUNNING",
      "usedScene": {
        "abilities": \[
          "EntryAbility"
        \],
        "when": "inuse"
      },
      "reason": "$string:location\_use"
    }
    
2.  在module.json5文件中为需要使用长时任务的UIAbility声明长时任务类型backgroundModes为location。
    
    "abilities": \[
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility\_desc",
        "icon": "$media:layered\_image",
        "label": "$string:EntryAbility\_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start\_window\_background",
        "exported": true,
        "backgroundModes": \["location"\],_// 声明支持定位后台模式_
        "skills": \[
          {
            "entities": \[
              "entity.system.home"
            \],
            "actions": \[
              "ohos.want.action.home"
            \]
          }
        \]
      }
    \],
    
3.  添加后台应用的相关信息。
    
    let wantAgentInfo: wantAgent.WantAgentInfo = {
     _ // 添加需要被拉起应用的bundleName和abilityName_
      wants: \[
        {
          bundleName: 'com.example.backgroundtask',
          abilityName: 'EntryAbility'
        }
      \],
      _// 指定点击通知栏消息后的动作是拉起ability_
      actionType: wantAgent.OperationType.START\_ABILITY,
     _ // 使用者自定义的一个私有值_
      requestCode: 0,
     _ // 点击通知后，动作执行属性_
      actionFlags: \[wantAgent.WantAgentFlags.UPDATE\_PRESENT\_FLAG\],
    };
    
4.  向用户申请位置权限。
    
    _// 启动定位之前先申请位置权限_
    await reqPermissionsFromUser(\['ohos.permission.APPROXIMATELY\_LOCATION', 'ohos.permission.LOCATION'\],
      this.getUIContext().getHostContext() as common.UIAbilityContext);
    
5.  启动长时任务时，长时任务类型配置为location。
    
    let list: Array<string> = \['location'\]; _// 注意长时任务类型_
    backgroundTaskManager.startBackgroundRunning(this.context, list, wantAgentObj)
      .then(async () => {
        console.info('Operation startBackgroundRunning succeeded');
      _  // 此处执行具体的长时任务逻辑，该示例中为获取位置信息_
        if (!geoLocationManager.isLocationEnabled()) {
          this.message = 'permission error';
        }
        let request: geoLocationManager.ContinuousLocationRequest = {
          'interval': 1,
          'locationScenario': geoLocationManager.UserActivityScenario.NAVIGATION
        };
        try {
          geoLocationManager.on('locationChange', request, (location: geoLocationManager.Location) => {
            this.latitude = location.latitude;
            this.longitude = location.longitude;
          });
        } catch (err) {
          this.message = JSON.stringify(err);
        }
      })
      .catch((error: BusinessError) => {
        console.error(\`Failed to Operation startBackgroundRunning. code is ${error.code} message is ${error.message}\`);
      });
    

完整代码：

import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { abilityAccessCtrl, common, Permissions, WantAgent, wantAgent } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { geoLocationManager } from '@kit.LocationKit';

async function reqPermissionsFromUser(permissions: Array<Permissions>,
  context: common.UIAbilityContext): Promise<boolean> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
 _ // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗_
  let data = await atManager.requestPermissionsFromUser(context, permissions);
  let grantStatus: Array<number> = data.authResults;
  let length: number = grantStatus.length;
  for (let i = 0; i < length; i++) {
    if (grantStatus\[i\] === 0) {
      return true; _// 用户授权，可以继续访问目标操作_
    }
  }
  return false; _// 授权失败_
}

@Entry
@Component
struct Index {
  @State message: string = 'ContinuousTask';
  @State latitude: number = 0;
  @State longitude: number = 0;
  _// 通过getUIContext().getHostContext()方法，来获取page所在的UIAbility上下文_
  private context: Context | undefined = this.getUIContext().getHostContext();

  startContinuousTask() {
    let wantAgentInfo: wantAgent.WantAgentInfo = {
    _  // 添加需要被拉起应用的bundleName和abilityName_
      wants: \[
        {
          bundleName: 'com.example.backgroundtask',
          abilityName: 'EntryAbility'
        }
      \],
     _ // 指定点击通知栏消息后的动作是拉起ability_
      actionType: wantAgent.OperationType.START\_ABILITY,
     _ // 使用者自定义的一个私有值_
      requestCode: 0,
     _ // 点击通知后，动作执行属性_
      actionFlags: \[wantAgent.WantAgentFlags.UPDATE\_PRESENT\_FLAG\],
    };

    try {
      _// 通过wantAgent模块下getWantAgent方法获取WantAgent对象_
      wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
        try {
          let list: Array<string> = \['location'\]; _// 注意长时任务类型_
          backgroundTaskManager.startBackgroundRunning(this.context, list, wantAgentObj)
            .then(async () => {
              console.info('Operation startBackgroundRunning succeeded');
             _ // 此处执行具体的长时任务逻辑，该示例中为获取位置信息_
              if (!geoLocationManager.isLocationEnabled()) {
                this.message = 'permission error';
              }
              let request: geoLocationManager.ContinuousLocationRequest = {
                'interval': 1,
                'locationScenario': geoLocationManager.UserActivityScenario.NAVIGATION
              };
              try {
                geoLocationManager.on('locationChange', request, (location: geoLocationManager.Location) => {
                  this.latitude = location.latitude;
                  this.longitude = location.longitude;
                });
              } catch (err) {
                this.message = JSON.stringify(err);
              }
            })
            .catch((error: BusinessError) => {
              console.error(\`Failed to Operation startBackgroundRunning. code is ${error.code} message is ${error.message}\`);
            });
        } catch (error) {
          console.error(\`Failed to Operation startBackgroundRunning. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}\`);
        }
      });
    } catch (error) {
      console.error(\`Failed to Operation getWantAgent. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}\`);
    }
  }

  stopContinuousTask() {
    backgroundTaskManager.stopBackgroundRunning(this.context).then(async () => {
      console.info(\`Succeeded in operationing stopBackgroundRunning.\`);
    }).catch((err: BusinessError) => {
      console.error(\`Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}\`);
    });
  }

  build() {
    Row() {
      Column() {
        Button('启动后台定位任务')
          .width(250)
          .height(40)
          .onClick(async () => {
            _// 启动定位之前先申请位置权限_
            await reqPermissionsFromUser(\['ohos.permission.APPROXIMATELY\_LOCATION', 'ohos.permission.LOCATION'\],
              this.getUIContext().getHostContext() as common.UIAbilityContext);
            this.startContinuousTask();
          });

        Button('取消后台长时任务')
          .margin({ top: 10, bottom: 10 })
          .width(250)
          .height(40)
          .onClick(() => {
            this.stopContinuousTask();
          });

        Text(\`latitude: ${this.latitude}\`);
        Text(\`longitude: ${this.longitude}\`);
      }
      .width('100%');
    }
    .height('100%');
  }
}