第39篇:隐私协议与合规——隐私协议页面实现

img

一、引言

隐私协议(Privacy Policy)是应用合规的重要组成部分,它向用户明确说明应用收集哪些数据、如何使用这些数据、如何保护用户隐私。在鸿蒙生态中,隐私合规更是应用上架的必要条件。DriverLicenseExam 项目实现了完整的隐私协议功能,包括协议内容管理、协议页面展示、用户授权管理。本文将深入解析其实现。

二、隐私协议合规要求

2.1 鸿蒙应用的合规要求

根据鸿蒙应用市场的上架要求,应用需要满足以下隐私合规要求:

  1. 隐私协议明确:清晰说明数据收集和使用规则
  2. 用户同意机制:在收集数据前获得用户明确同意
  3. 协议可查阅:用户随时可以查看隐私协议
  4. 撤回同意:用户有权撤回对隐私协议的同意

2.2 项目中的隐私合规设计

项目在以下环节实现了隐私合规:

应用启动 → 首次使用 → 隐私协议展示 → 用户同意 → 正常使用
                                    ↓
                                不同意 → 退出应用

用户使用中 → 设置页面 → 隐私协议查阅 → 随时可查看

三、隐私协议数据管理

3.1 rawfile 数据存储

隐私协议的 URL 等信息存储在 AppScope/resources/rawfile/data.json 中:

// AppScope/resources/rawfile/data.json
{
  "privacy_url": "https://developer.huawei.com/consumer/cn/privacy/",
  "user_agreement": "https://developer.huawei.com/consumer/cn/agreement/"
}

3.2 读取协议 URL

// 从 rawfile 读取协议地址
getProtocolUrl(privacyUrl: string): string {
  try {
    const context = this.getUIContext().getHostContext() as Context;
    // 获取原始文件内容
    const value: Uint8Array = context.resourceManager.getRawFileContentSync('data.json');
    // 转换为字符串并解析 JSON
    const jsonStr = buffer.from(value.buffer).toString();
    const config = JSON.parse(jsonStr);
    return config[privacyUrl] as string;
  } catch (error) {
    hilog.error(this.domainId, this.logTag,
      'getProtocolUrl Error: ' + error.message);
    return '';
  }
}

这个方法的执行流程如下:

  1. 获取 Context:通过 getUIContext().getHostContext() 获取上下文
  2. 读取 rawfilegetRawFileContentSync 同步读取原始文件
  3. 类型转换Uint8Arraybufferstring
  4. JSON 解析:解析为对象,获取指定字段

3.3 多语言支持

协议地址可以支持多语言,通过系统语言环境自动切换:

// 获取系统语言环境
const systemLanguage: string = (this.getUIContext().getHostContext() as common.UIExtensionContext)
  .config.language ?? '';

// 拼接语言参数
const protocolUrl = this.getProtocolUrl('privacy_url') + systemLanguage;

四、隐私协议页面

4.1 PrivacyAgreement 组件

// PrivacyAgreement.ets
@ComponentV2
export struct PrivacyAgreement {
  @Local protocolUrl: string = '';
  private webController: WebController = new WebController();

  aboutToAppear() {
    // 获取传递的协议 URL
    const params = this.vm.navStack.getParamByIndex(
      this.vm.navStack.size() - 1) as Record<string, Object>;
    if (params && params.protocolUrl) {
      this.protocolUrl = params.protocolUrl as string;
    } else {
      // 默认获取隐私协议
      this.protocolUrl = this.getProtocolUrl('privacy_url');
    }
  }

  build() {
    NavDestination() {
      Column() {
        // Web 组件展示协议内容
        if (this.protocolUrl) {
          Web({ src: this.protocolUrl, controller: this.webController })
            .width('100%')
            .height('100%')
            .onPageBegin((event) => {
              Logger.info('PrivacyAgreement', 'Page load started');
            })
            .onPageEnd((event) => {
              Logger.info('PrivacyAgreement', 'Page load completed');
            })
            .onErrorReceive((event) => {
              Logger.error('PrivacyAgreement', 'Page load error: ' + event.error);
            });
        } else {
          // 加载失败提示
          Column() {
            Image($r('app.media.ic_error')).width(48).height(48);
            Text('协议加载失败,请检查网络连接')
              .fontSize(14)
              .fontColor('#999')
              .margin({ top: 16 });
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center);
        }
      }
      .width('100%')
      .height('100%');
    }
    .title('隐私协议');
  }
}

4.2 Web 组件的使用

ArkUI 的 Web 组件用于加载网页内容,支持以下特性:

Web({ src: this.protocolUrl, controller: this.webController })
  .width('100%')
  .height('100%')
  // 页面加载事件
  .onPageBegin((event) => {})
  .onPageEnd((event) => {})
  .onErrorReceive((event) => {})
  // 加载进度
  .onProgressChange((progress) => {})
  // 是否支持 JavaScript
  .javaScriptAccess(true);

五、协议勾选与授权

5.1 登录页面的协议勾选

在华为账号一键登录页面,用户需要勾选同意隐私协议才能登录:

// QuickLoginPage.ets 中的协议勾选
Flex({
  justifyContent: FlexAlign.Center,
  alignItems: ItemAlign.Start,
}) {
  // 复选框
  Row() {
    Checkbox({ name: 'privacyCheckbox', group: 'privacyCheckboxGroup' })
      .width(24).height(24)
      .selectedColor($r('sys.color.comp_background_emphasize'))
      .select(this.isSelected)
      .enabled(this.enableStatus)
      .onChange((value: boolean) => {
        if (value) {
          this.isSelected = true;
          this.showPopUp = false;
          // 设置协议状态为已同意
          this.controller.setAgreementStatus(
            loginComponentManager.AgreementStatus.ACCEPTED);
        } else {
          this.isSelected = false;
          // 设置协议状态为未同意
          this.controller.setAgreementStatus(
            loginComponentManager.AgreementStatus.NOT_ACCEPTED);
        }
      });
  }
  .padding({ right: 12 })
  .bindPopup(this.showPopUp, {
    message: '请阅读并同意相关协议',
    onStateChange: (event) => {
      this.showPopUp = event.isVisible;
    },
    mask: false,
  });

  // 协议文本
  Row() {
    Text() {
      ForEach(this.privacyText, (item: loginComponentManager.PrivacyText) => {
        if (item?.type === loginComponentManager.TextType.PLAIN_TEXT) {
          Span(item?.text)
            .fontColor($r('sys.color.ohos_id_color_text_secondary'));
        } else if (item?.type === loginComponentManager.TextType.RICH_TEXT) {
          Span(item?.text)
            .fontColor($r('sys.color.ohos_id_color_text_primary'))
            .onClick(() => {
              this.jumpToPrivacyWebView(item);
            });
        }
      });
    }
    .maxFontScale(1);
  }
}

5.2 协议文本配置

协议文本通过 PrivacyText 数组配置,支持富文本点击:

privacyText: loginComponentManager.PrivacyText[] = [
  {
    text: $r('app.string.read_and_agree'),
    type: loginComponentManager.TextType.PLAIN_TEXT,
  },
  {
    text: $r('app.string.app_user_agreement'),
    type: loginComponentManager.TextType.RICH_TEXT,
    tag: '',  // 用户协议标签
  },
  {
    text: $r('app.string.app_privacy_policy'),
    type: loginComponentManager.TextType.RICH_TEXT,
    tag: 'privacy_policy',  // 隐私协议标签
  },
  {
    text: $r('app.string.and'),
    type: loginComponentManager.TextType.PLAIN_TEXT,
  },
  {
    text: $r('app.string.huaweiId_user_authentication_protocol'),
    type: loginComponentManager.TextType.RICH_TEXT,
    tag: this.getProtocolUrl(PRIVACY_URL),  // 华为账号认证协议
  },
  {
    text: $r('app.string.end'),
    type: loginComponentManager.TextType.PLAIN_TEXT,
  },
];

5.3 协议跳转

// 点击协议文本时跳转到对应页面
jumpToPrivacyWebView(privacyText: loginComponentManager.PrivacyText) {
  if (!this.enableStatus) return;

  if (privacyText.tag === 'privacy_policy') {
    // 跳转到隐私协议本地页面
    this.vm.navStack.pushPathByName('PrivacyAgreement', undefined);
    return;
  }

  // 检查网络连接
  const hasNet: boolean = connection.hasDefaultNetSync();
  if (!hasNet) {
    const hint = '网络连接不可用,请检查网络设置';
    showToast(hint, this.getUIContext());
    return;
  }

  // 跳转到 Web 协议页面
  if (privacyText.tag && privacyText.tag !== '') {
    const systemLanguage = (this.getUIContext().getHostContext() as common.UIExtensionContext)
      .config.language ?? '';
    const params: Record<string, Object> = {
      'protocolUrl': privacyText.tag + systemLanguage
    };
    this.vm.navStack.pushPathByName('PrivacyAgreement', params);
  }
}

六、合规性设计要点

6.1 用户同意记录

在用户同意隐私协议后,应该记录同意状态,避免每次启动都要求用户同意:

// 保存用户同意状态
saveAgreementStatus() {
  const preferencesUtil = PreferencesUtil.getInstance();
  const preference = preferencesUtil.getPreferences(context);
  preferencesUtil.preferencesPut(preference, 'agreementAccepted', true);
  preferencesUtil.preferencesPut(preference, 'agreementDate', Date.now().toString());
}

// 检查用户是否已同意
isAgreementAccepted(): boolean {
  const preferencesUtil = PreferencesUtil.getInstance();
  const preference = preferencesUtil.getPreferences(context);
  return preferencesUtil.getPreferencesValue(preference, 'agreementAccepted') === true;
}

6.2 隐私协议更新通知

当隐私协议更新时,需要重新获得用户同意:

// 协议版本管理
checkAgreementUpdate() {
  const currentVersion = '1.0';  // 当前协议版本
  const preferencesUtil = PreferencesUtil.getInstance();
  const preference = preferencesUtil.getPreferences(context);
  const acceptedVersion = preferencesUtil.getPreferencesValue(preference, 'agreementVersion');

  if (acceptedVersion !== currentVersion) {
    // 协议已更新,需要重新同意
    showPrivacyAgreementUpdate();
  }
}

七、合规检查清单

在应用上架前,建议对照以下清单进行合规检查:

  • 隐私协议内容完整,覆盖数据收集、使用、共享、保护等条款
  • 隐私协议在应用首次启动时展示
  • 用户在使用敏感功能前必须同意隐私协议
  • 用户拒绝隐私协议时,应用退出或限制功能
  • 用户随时可以查看隐私协议
  • 隐私协议更新后,需要重新获得用户同意
  • 用户同意状态已持久化存储

八、总结

隐私协议与合规是应用中不可忽视的重要环节。DriverLicenseExam 项目通过以下方式实现了完整的隐私合规流程:

  1. 协议数据管理:通过 rawfile 存储协议 URL,支持多语言
  2. 协议页面展示:使用 Web 组件加载协议内容
  3. 用户授权管理:通过 Checkbox 和 AgreementStatus 控制授权状态
  4. 协议跳转:支持点击协议文本跳转到对应页面
  5. 合规持久化:记录用户同意状态,协议更新时重新获取同意

关键源码文件:

  • products/entry/src/main/ets/pages/mine/PrivacyAgreement.ets — 隐私协议页面
  • products/entry/src/main/ets/pages/mine/QuickLoginPage.ets — 登录页协议勾选
  • products/entry/src/main/ets/pages/mine/Setting.ets — 设置页协议入口
  • AppScope/resources/rawfile/data.json — 协议 URL 配置
  • commons/commonLib/src/main/ets/utils/PreferencesUtil.ets — 偏好持久化
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐