应用在首次打开或运行中,在用户未主动点击权限对应的相关功能或服务时,提前向用户弹窗申请开启【读取全部日程】权限,不符合相关法律法规要求。该问题已被修复。

Signed-off-by: laoyang <laoyang@example.com>
This commit is contained in:
laoyang
2026-09-16 20:54:41 +08:00
parent 76d82886e4
commit 353473127a
8 changed files with 163 additions and 71 deletions
@@ -3,6 +3,7 @@
import { router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { notificationManager } from '@kit.NotificationKit';
import { DavAccount, AccountStore, CalSource, BookPalette } from '../common/AccountStore';
import { EventDb, LocalEvent } from '../common/EventDb';
import { AppSettings } from '../common/AppSettings';
@@ -272,6 +273,11 @@ struct EventEditPage {
}
// 日志仅记录 uid 与提醒/重复设置,不落盘日程标题(避免隐私内容进入可备份的 sync.log)
LogUtil.write(`本地保存日程(标题已脱敏)uid=${e.uid} 提醒=${e.reminders.join('/')}分钟 重复=${e.rrule === '' ? '否' : e.rrule}`);
// 设置了提醒 → 此刻才申请通知权限(用户刚主动使用了依赖通知的功能,
// 符合《审核指南》7.17"在用户主动点击对应功能时申请",不随 App 启动自动弹窗)
if (e.reminders.length > 0) {
await this.ensureNotifyPermission();
}
// 立即刷新提醒(不等下一轮同步),保证刚保存的提醒马上生效
try {
await ReminderService.refreshReminders(context as common.UIAbilityContext);
@@ -290,6 +296,27 @@ struct EventEditPage {
this.isSaving = false;
}
/**
* 确认通知权限已开启 —— 仅"用户保存了带提醒的日程"这条路径会调用。
* 日程提醒依赖系统通知,用户刚主动设置了提醒,此时申请权限与其使用目的完全一致。
* ⚠️ 严禁在 App 启动 / 首次打开时调用(《审核指南》7.17:不得提前弹窗申请权限)。
*/
private async ensureNotifyPermission(): Promise<void> {
try {
const enabled: boolean = await notificationManager.isNotificationEnabled();
if (enabled) {
return;
}
const context = this.getUIContext().getHostContext();
if (context === undefined) {
return;
}
await notificationManager.requestEnableNotification(context as common.UIAbilityContext);
} catch (err) {
// 用户拒绝或未弹窗:日程照常保存,只是提醒可能不弹出(设置页可再开启)
}
}
/**
* 删除前二次确认。
* 删除日程不可撤销,且会同时从本地库与服务器(DAV)移除,故破坏性操作前必须显式确认,
+9 -34
View File
@@ -5,7 +5,6 @@ import { mediaquery, router } from '@kit.ArkUI';
import { common, abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';
import { geoLocationManager } from '@kit.LocationKit';
import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
import { notificationManager } from '@kit.NotificationKit';
import { DavAccount, AccountStore, CalSource, TYPE_CALDAV } from '../common/AccountStore';
import { DisplayEvent, CalendarDataService } from '../common/CalendarDataService';
import { SyncEngine } from '../common/SyncEngine';
@@ -84,7 +83,6 @@ struct Index {
// ---- 隐私政策与用户协议首启同意(未同意前不申请任何权限、不加载数据) ----
@State policyChecked: boolean = false; // 是否已读取过同意状态(避免首帧闪烁)
@State policyAgreed: boolean = false; // 是否已同意《隐私政策》与《用户协议》
private notifAsked: boolean = false; // 通知权限是否已申请(同意后才申请)
@State showTips: boolean = false; // 首启功能引导(整页覆盖层:遮罩 + 底部卡片 + Swiper 三页)
// ---- 内置文档浏览器(《隐私政策》/《用户服务协议》,整页 Web 覆盖层,不跳出应用) ----
@State showDoc: boolean = false;
@@ -211,40 +209,17 @@ struct Index {
}
}
private initPermissionAndLoad(): void {
const context = this.getUIContext().getHostContext();
if (context === undefined) {
return;
}
this.reloadAll();
this.ensureNotificationPermission(context as common.UIAbilityContext);
if (!this.permissionAsked) {
this.permissionAsked = true;
// 申请读取全部日历权限(用于混合展示系统日程)
CalendarDataService.ensureSystemCalendarPermission(context as common.UIAbilityContext)
.then((): void => {
this.reloadAll();
});
}
}
/**
* 申请通知权限(日程提醒、后台同步常驻通知都依赖它)
* 必须在用户同意《隐私政策》之后才调用 —— 同意前不得申请任何权限。
* 启动/同意隐私政策后的初始化:**只加载本地库与 DAV 数据**
*
* ⚠️ 合规约束(《审核指南》7.17):这里**不申请任何权限**。
* 权限必须由用户主动点击对应功能时申请,否则会被判定为"未主动点击即提前弹窗申请":
* - 读取全部日程(日历)→ 设置页「混合显示系统日历」开关 / 「备份到 CalDAV 日历本」
* - 通知 → 保存带提醒的日程 / 开启「后台持续同步」/ 设置页「去开启通知」
* 本方法只负责加载数据;系统日历来源由 CalendarDataService.loadSources 按开关与授权状态自行决定是否读取。
*/
private ensureNotificationPermission(context: common.UIAbilityContext): void {
if (this.notifAsked) {
return;
}
this.notifAsked = true;
notificationManager.isNotificationEnabled()
.then((enabled: boolean): Promise<void> => {
if (!enabled) {
return notificationManager.requestEnableNotification(context).catch((): void => {});
}
return Promise.resolve();
})
.catch((): void => {});
private initPermissionAndLoad(): void {
this.reloadAll();
}
/**
+66 -2
View File
@@ -5,6 +5,7 @@ import { common } from '@kit.AbilityKit';
import { notificationManager } from '@kit.NotificationKit';
import { AppSettings } from '../common/AppSettings';
import { CardDataService } from '../common/CardDataService';
import { CalendarDataService } from '../common/CalendarDataService';
import { BackgroundSyncService } from '../common/BackgroundSyncService';
import { AccountStore, DavAccount } from '../common/AccountStore';
import { ReminderService } from '../common/ReminderService';
@@ -49,6 +50,15 @@ struct SettingsPage {
this.context = ctx;
AppSettings.getShowSystemCalendar(ctx).then((v: boolean): void => {
this.showSystem = v;
// 状态自洽:若记录为"开",但权限已被用户在系统设置里回收 → 自动回落为"关",避免开关与实际不符
if (v) {
CalendarDataService.hasSystemCalendarPermission().then((granted: boolean): void => {
if (!granted) {
this.showSystem = false;
AppSettings.setShowSystemCalendar(ctx, false);
}
});
}
});
AppSettings.getSyncIntervalMinutes(ctx).then((v: number): void => {
this.intervalMinutes = v;
@@ -87,6 +97,27 @@ struct SettingsPage {
});
}
/**
* 确认通知权限已开启 —— **只在用户主动使用依赖通知的功能时调用**
* (保存带提醒的日程 / 开启后台持续同步 / 点击设置页"去开启通知")。
* ⚠️ 严禁在 App 启动、首次打开等用户未操作的时机调用(《审核指南》7.17)。
*/
private async ensureNotifyPermission(): Promise<void> {
if (this.context === undefined) {
return;
}
try {
const enabled: boolean = await notificationManager.isNotificationEnabled();
if (!enabled) {
await notificationManager.requestEnableNotification(
this.context as common.UIAbilityContext);
}
} catch (err) {
// 用户拒绝或系统未弹窗:不阻断功能,仅状态仍显示为未开启
}
this.refreshNotifyState();
}
/** 加载全部 DAV 日历本(含探测到的写权限,只读/静音标记管理用) */
private async loadAllBooks(): Promise<void> {
if (this.context === undefined) {
@@ -198,6 +229,18 @@ struct SettingsPage {
if (this.context === undefined) {
return;
}
// "备份到 CalDAV" 需要读取手机系统(本地)日历 → 只在用户主动选择该模式时申请权限
if (mode === 'backup') {
const granted: boolean = await CalendarDataService.requestSystemCalendarPermission(
this.context as common.UIAbilityContext);
if (!granted) {
this.sysMode = 'display'; // 回弹选择,保持与实际授权状态一致
this.getUIContext().getPromptAction().showToast({
message: '未获得"读取全部日程"权限,无法开启系统日历备份'
});
return;
}
}
this.sysMode = mode;
await AppSettings.setSysCalMode(this.context, mode);
if (mode === 'backup' && this.backupKey === '') {
@@ -223,9 +266,26 @@ struct SettingsPage {
.showToast({ message: '备份目标已更新,下次同步生效' });
}
private async saveShowSystem(value: boolean): Promise<void> { if (this.context === undefined) {
/**
* 保存「混合显示系统日历」开关。
* ⚠️ 合规约束(《审核指南》7.17):读取系统日历依赖"读取全部日程"敏感权限,
* **只有在用户主动打开该开关时才申请**;未授权则开关回弹、不保存设置。
*/
private async saveShowSystem(value: boolean): Promise<void> {
if (this.context === undefined) {
return;
}
if (value) {
const granted: boolean = await CalendarDataService.requestSystemCalendarPermission(
this.context as common.UIAbilityContext);
if (!granted) {
this.showSystem = false; // 回弹开关,保持与实际授权状态一致
this.getUIContext().getPromptAction().showToast({
message: '未获得"读取全部日程"权限,无法混合显示系统日历'
});
return;
}
}
await AppSettings.setShowSystemCalendar(this.context, value);
this.getUIContext().getPromptAction()
.showToast({ message: value ? '已开启系统日历混合显示,返回首页生效' : '已关闭系统日历混合显示,返回首页生效' });
@@ -245,6 +305,10 @@ struct SettingsPage {
if (this.context === undefined) {
return;
}
// 后台持续同步依赖常驻通知 → 在用户主动开启时才申请通知权限(不随启动自动弹窗)
if (value) {
await this.ensureNotifyPermission();
}
try {
await BackgroundSyncService.setBackgroundSync(
this.context as common.UIAbilityContext, value);
@@ -447,7 +511,7 @@ struct SettingsPage {
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.text_primary'))
Text('选择手机系统日历日程的处理方式')
Text('选择手机系统日历日程的处理方式(需授权读取系统日历)')
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
}