HarmonyOS鸿蒙Next中同意隐私前获取设备和位置信息

HarmonyOS鸿蒙Next中同意隐私前获取设备和位置信息 提审有个新的问题,有一个未同意隐私前获取设备和位置信息,审核意见: 请在用户同意隐私政策后,再申请获取用户个人信息及权限。 您可参考《审核指南》第7.5项:https://developer.huawei.com/consumer/cn/doc/app/50104-07 APP常见个人信息保护问题FAQ您可参考: https://developer.huawei.com/consumer/cn/doc/app/FAQ-fag-01#h1-1683539557872-1


更多关于HarmonyOS鸿蒙Next中同意隐私前获取设备和位置信息的实战教程也可以访问 https://www.itying.com/category-93-b0.html

4 回复

【背景知识】 获取位置信息需要得到相关权限,可以先学习应用权限管控概述。具体实现主要使用Location Kit提供的能力,请阅读Location Kit简介

【解决方案】

  1. 打开设备定位开关。需要用户手动打开位置开关,开发者可以通过isLocationEnabled接口判断位置服务是否开启,然后引导用户手动开启。

  2. 声明并获取相关位置权限。主要涉及三种权限:

    • ohos.permission.LOCATION用于获取精准位置。
    • ohos.permission.APPROXIMATELY_LOCATION用于获取模糊位置。
    • ohos.permission.LOCATION_IN_BACKGROUND用于后台定位的场景。

    开发者根据需要在moudle.json5文件中配置进行声明,并获取用户明确授权方可使用,获取授权有两种方式:(1)通过requestPermissionsFromUser接口向用户弹窗以获得授权。(2)跳转到应用权限设置页面,打开开关进行授权,参考如下示例代码。

Button('跳转应用权限设置页面')
  .onClick(() =>{
    let context = getContext(this) as common.UIAbilityContext;
    context.startAbility({
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri:'application_info_entry',
      parameters: {
        pushParams: "zouzou.Location_Kit.Location_Kit1"  // 当前包名
      }
    })
  })
  1. 获取当前位置有如下2种,参考获取设备的位置信息开发指导: (1)getLastLocation获取系统缓存的最新位置,可减少系统功耗,推荐使用。 (2)getCurrentLocation获取当前位置。

  2. 持续获取最新的位置,前台和后台需要做不同的调整。 (1)前台持续获取位置信息。直接调用geoLocationManager.on(‘locationChange’)周期性持续监听最新的位置。参考获取设备的位置信息开发指导。 (2)后台持续获取位置信息。需搭配长时任务使用,选择申请长时任务类型为LOCATION。参考长时任务开发指南。示例代码如下:

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

function callback(info: backgroundTaskManager.ContinuousTaskCancelInfo) {
  // 长时任务id
  console.info('OnContinuousTaskCancel callback id ' + info.id);
  // 长时任务取消原因
  console.info('OnContinuousTaskCancel callback reason ' + info.reason);
}

const permissions: Array<Permissions> = ['ohos.permission.LOCATION','ohos.permission.APPROXIMATELY_LOCATION'];
// 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
  atManager.requestPermissionsFromUser(context, permissions).then((data) => {
    let grantStatus: Array<number> = data.authResults;
    let length: number = grantStatus.length;
    for (let i = 0; i < length; i++) {
      if (grantStatus[i] === 0) {
        // 用户授权,可以继续访问目标操作
      } else {
        // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
        return;
      }
    }
    // 授权成功
  }).catch((err: BusinessError) => {
    console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
  })
}

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

  aboutToAppear() {
    // 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
    const context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
    reqPermissionsFromUser(permissions, context);
  }

  OnContinuousTaskCancel() {
    try {
      backgroundTaskManager.on("continuousTaskCancel", callback);
      console.info(`Succeeded in operationing OnContinuousTaskCancel.`);
    } catch (error) {
      console.error(`Operation OnContinuousTaskCancel failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
    }
  }
  
  OffContinuousTaskCancel() {
    try {
      // callback参数不传,则取消所有已注册的回调
      backgroundTaskManager.off("continuousTaskCancel", callback);
      console.info(`Succeeded in operationing OffContinuousTaskCancel.`);
    } catch (error) {
      console.error(`Operation OffContinuousTaskCancel failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
    }
  }

  startContinuousTask() {
    let wantAgentInfo: wantAgent.WantAgentInfo = {
      // 点击通知后,将要执行的动作列表
      // 添加需要被拉起应用的bundleName和abilityName
      wants: [
        {
          bundleName: "com.example.swiper",
          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"];
          // let list: Array<string> = ["bluetoothInteraction"]; 长时任务类型包含bluetoothInteraction,CAR_KEY子类型合法
          backgroundTaskManager.startBackgroundRunning(this.context, list, wantAgentObj).then((res: backgroundTaskManager.ContinuousTaskNotification) => {
            console.info("Operation startBackgroundRunning succeeded");
            // 此处执行具体的长时任务逻辑,如录音,录制等。
            let request: geoLocationManager.ContinuousLocationRequest= {
              'interval': 1,
              'locationScenario': geoLocationManager.UserActivityScenario.NAVIGATION
            }
            let locationCallback = (location:geoLocationManager.Location):void => {
              console.info('locationCallback: data: ' + JSON.stringify(location));
            };
            try {
              geoLocationManager.on('locationChange', request, locationCallback);
            } catch (err) {
              console.error("errCode:" + 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(() => {
      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() {
        Text("Index")
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button() {
          Text('申请长时任务').fontSize(25).fontWeight(FontWeight.Bold)
        }
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0D9FFB')
        .width(250)
        .height(40)
        .onClick(() => {
          // 通过按钮申请长时任务
          this.startContinuousTask();
        })
        Button() {
          Text('取消长时任务').fontSize(25).fontWeight(FontWeight.Bold)
        }
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0D9FFB')
        .width(250)
        .height(40)
        .onClick(() => {
          // 此处结束具体的长时任务的执行
          // 通过按钮取消长时任务
          this.stopContinuousTask();
        })
        Button() {
          Text('注册长时任务取消回调').fontSize(25).fontWeight(FontWeight.Bold)
        }
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0D9FFB')
        .width(250)
        .height(40)
        .onClick(() => {
          // 通过按钮注册长时任务取消回调
          this.OnContinuousTaskCancel();
        })
        Button() {
          Text('取消注册长时任务取消回调').fontSize(25).fontWeight(FontWeight.Bold)
        }
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0D9FFB')
        .width(250)
        .height(40)
        .onClick(() => {
          // 通过按钮取消注册长时任务取消回调
          this.OffContinuousTaskCancel();
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}

更多关于HarmonyOS鸿蒙Next中同意隐私前获取设备和位置信息的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


【背景知识】
[@ohos.abilityAccessCtrl(程序访问控制管理)](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-abilityaccessctrl):程序访问控制提供应用程序的权限校验和管理能力。

【参考方案】:
可参考考勤打卡位置获取示例,基于位置服务实现地理位置获取的功能,以及用户权限申请的交互效果。

  1. 调用requestPermissionsFromUser接口第一次拉起弹框请求用户授权;若用户拒绝授权,调用requestPermissionOnSetting接口,二次拉起权限设置弹框请求用户授权;若用户再次拒绝授权,则拉起跳转到设置页面的权限申请弹窗。
// 申请用户授权
async requestPermissions(context: UIContext, permissions: Array<Permissions>): Promise<boolean | undefined> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  try {
    let data = await atManager.requestPermissionsFromUser(context.getHostContext() as common.UIAbilityContext, permissions)
    return data.dialogShownResults ? data.dialogShownResults[0] : undefined; // 返回请求是否有弹窗
  } catch (e) {
    return undefined;
  }
}
// 2次申请用户授权
requestPermissionsOnSetting(context: UIContext, permissions: Array<Permissions>) {
  let keyPerms = JSON.stringify(permissions)
  let store = preferences.getPreferencesSync(context.getHostContext(), { name: 'permsHasOnSetting' });
  if (!store.hasSync(keyPerms)) {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    atManager.requestPermissionOnSetting(context.getHostContext(), permissions)
  }
}

在HarmonyOS Next中,系统在用户同意隐私协议前不会获取设备或位置信息。所有涉及硬件和位置的数据访问必须经过用户明确授权,遵循隐私合规设计。系统通过权限管理机制确保在未获得同意时不采集任何敏感信息。

根据HarmonyOS Next的隐私合规要求,应用必须在用户明确同意隐私政策后,才能申请获取设备信息(如设备标识符)和位置权限。审核未通过是因为检测到应用在隐私协议弹窗展示前就调用了相关API。

建议检查代码中权限申请(如ohos.permission.LOCATION)和设备信息获取(如getDeviceId)的调用时机,确保这些操作仅在用户点击“同意”后执行。可参考华为提供的隐私合规开发指南,调整权限请求逻辑,确保符合规范。

回到顶部