鸿蒙掌上驾考宝典应用开发39:隐私协议与合规——隐私协议页面实现
·
第39篇:隐私协议与合规——隐私协议页面实现

一、引言
隐私协议(Privacy Policy)是应用合规的重要组成部分,它向用户明确说明应用收集哪些数据、如何使用这些数据、如何保护用户隐私。在鸿蒙生态中,隐私合规更是应用上架的必要条件。DriverLicenseExam 项目实现了完整的隐私协议功能,包括协议内容管理、协议页面展示、用户授权管理。本文将深入解析其实现。
二、隐私协议合规要求
2.1 鸿蒙应用的合规要求
根据鸿蒙应用市场的上架要求,应用需要满足以下隐私合规要求:
- 隐私协议明确:清晰说明数据收集和使用规则
- 用户同意机制:在收集数据前获得用户明确同意
- 协议可查阅:用户随时可以查看隐私协议
- 撤回同意:用户有权撤回对隐私协议的同意
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 '';
}
}
这个方法的执行流程如下:
- 获取 Context:通过
getUIContext().getHostContext()获取上下文 - 读取 rawfile:
getRawFileContentSync同步读取原始文件 - 类型转换:
Uint8Array→buffer→string - 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 项目通过以下方式实现了完整的隐私合规流程:
- 协议数据管理:通过 rawfile 存储协议 URL,支持多语言
- 协议页面展示:使用 Web 组件加载协议内容
- 用户授权管理:通过 Checkbox 和 AgreementStatus 控制授权状态
- 协议跳转:支持点击协议文本跳转到对应页面
- 合规持久化:记录用户同意状态,协议更新时重新获取同意
关键源码文件:
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— 偏好持久化
更多推荐




所有评论(0)