应用在首次打开或运行中,在用户未主动点击权限对应的相关功能或服务时,提前向用户弹窗申请开启【读取全部日程】权限,不符合相关法律法规要求。该问题已被修复。
Signed-off-by: laoyang <laoyang@example.com>
This commit is contained in:
+2
-2
@@ -2,8 +2,8 @@
|
||||
"app": {
|
||||
"bundleName": "synccalendar.yangyq.net",
|
||||
"vendor": "yangyq",
|
||||
"versionCode": 100,
|
||||
"versionName": "0.0.1",
|
||||
"versionCode": 200,
|
||||
"versionName": "0.0.2",
|
||||
"buildVersion": "1",
|
||||
"icon": "$media:layered_image",
|
||||
"label": "$string:app_name"
|
||||
|
||||
@@ -365,14 +365,18 @@ export class AppSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否混合显示系统日历日程(默认开) */
|
||||
/**
|
||||
* 是否混合显示系统日历日程。
|
||||
* ⚠️ 默认**关闭**:读取系统日历需要"读取全部日程"敏感权限,按《审核指南》7.17
|
||||
* 只能在用户主动打开该开关时才申请,因此默认值必须与实际授权状态一致(无授权 = 关)。
|
||||
*/
|
||||
static async getShowSystemCalendar(context: common.Context): Promise<boolean> {
|
||||
try {
|
||||
const store: preferences.Preferences =
|
||||
await preferences.getPreferences(context, AppSettings.STORE);
|
||||
return await store.get(AppSettings.KEY_SHOW_SYSTEM, true) as boolean;
|
||||
return await store.get(AppSettings.KEY_SHOW_SYSTEM, false) as boolean;
|
||||
} catch (err) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// entry/src/main/ets/common/CalendarDataService.ets
|
||||
// 合并数据源:本地库(DAV 同步 + 本机事件) + 系统日历(只读展示)
|
||||
import { common, abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
|
||||
import { common, abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { calendarManager } from '@kit.CalendarKit';
|
||||
import { AccountStore, CalSource, BookPalette, LOCAL_CAL_KEY, DavAccount } from './AccountStore';
|
||||
@@ -29,15 +29,52 @@ export class DisplayEvent {
|
||||
}
|
||||
|
||||
export class CalendarDataService {
|
||||
/** 申请读取全部日历权限(混合展示系统日历需要) */
|
||||
static async ensureSystemCalendarPermission(context: common.UIAbilityContext): Promise<boolean> {
|
||||
/** 本功能依赖的日历权限:读取本应用日历 + 读取全部日历(即"读取全部日程") */
|
||||
private static readonly CALENDAR_PERMISSIONS: Permissions[] = [
|
||||
'ohos.permission.READ_CALENDAR',
|
||||
'ohos.permission.READ_WHOLE_CALENDAR'
|
||||
];
|
||||
|
||||
/**
|
||||
* 是否已获得"读取全部日程"权限。
|
||||
* **只检查、绝不弹窗** —— 供界面判断状态、决定是否需要引导用户主动授权。
|
||||
* 注意:checkAccessToken 只反映本应用授权状态,不会触发任何系统弹窗。
|
||||
*/
|
||||
static async hasSystemCalendarPermission(): Promise<boolean> {
|
||||
try {
|
||||
const atManager = abilityAccessCtrl.createAtManager();
|
||||
const permissions: Permissions[] = [
|
||||
'ohos.permission.READ_CALENDAR',
|
||||
'ohos.permission.READ_WHOLE_CALENDAR'
|
||||
];
|
||||
const result = await atManager.requestPermissionsFromUser(context, permissions);
|
||||
const info = bundleManager.getBundleInfoForSelfSync(
|
||||
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
|
||||
const tokenId: number = info.appInfo.accessTokenId;
|
||||
for (const p of CalendarDataService.CALENDAR_PERMISSIONS) {
|
||||
const status = await atManager.checkAccessToken(tokenId, p);
|
||||
if (status !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请"读取全部日程"权限(READ_CALENDAR + READ_WHOLE_CALENDAR)。
|
||||
*
|
||||
* ⚠️ 合规约束(《审核指南》7.17):**只能在用户主动点击使用对应功能时调用**,
|
||||
* 例如在设置页打开"混合显示系统日历"开关、把系统日历模式切到"备份到 CalDAV 日历本"。
|
||||
* 严禁在 App 启动 / 首次打开 / 未同意隐私政策等用户未主动操作的时机调用。
|
||||
*
|
||||
* 已授权时直接返回 true,不会再弹窗(先 checkAccessToken,避免多余弹窗)。
|
||||
*/
|
||||
static async requestSystemCalendarPermission(context: common.UIAbilityContext): Promise<boolean> {
|
||||
if (await CalendarDataService.hasSystemCalendarPermission()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const atManager = abilityAccessCtrl.createAtManager();
|
||||
const result = await atManager.requestPermissionsFromUser(
|
||||
context, CalendarDataService.CALENDAR_PERMISSIONS);
|
||||
for (const r of result.authResults) {
|
||||
if (r !== 0) {
|
||||
return false;
|
||||
|
||||
@@ -28,8 +28,8 @@ export struct TipsPanel {
|
||||
{
|
||||
icon: $r('app.media.tip_calendar'),
|
||||
title: '系统日历,也能一起管',
|
||||
desc: '手机系统日历可与 CalDAV 日历混合显示在同一时间轴;还能把系统日历本通过 CalDAV 同步备份,' +
|
||||
'一处编辑、处处更新。'
|
||||
desc: '手机系统日历可与 CalDAV 日历混合显示在同一时间轴,也能把系统日历本通过 CalDAV 同步备份,' +
|
||||
'一处编辑、处处更新。到「设置 → 混合显示系统日历」开启并授权即可。'
|
||||
},
|
||||
{
|
||||
icon: $r('app.media.tip_mute'),
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { window } from '@kit.ArkUI';
|
||||
import { notificationManager } from '@kit.NotificationKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { BackgroundSyncService } from '../common/BackgroundSyncService';
|
||||
import { LogUtil } from '../common/LogUtil';
|
||||
import { AppSettings } from '../common/AppSettings';
|
||||
|
||||
const DOMAIN = 0x0000;
|
||||
|
||||
@@ -19,24 +17,11 @@ export default class EntryAbility extends UIAbility {
|
||||
BackgroundSyncService.initOnLaunch(this.context);
|
||||
// 服务卡片"添加"按钮跳转:把 widgetAction=add 透传给主页面(在 AppStorage 中暂存)
|
||||
this.handleWidgetParam(want);
|
||||
// 通知权限:日程提醒、后台同步常驻通知都依赖它(系统只弹一次授权框)。
|
||||
// 必须在用户同意《隐私政策》之后才申请 —— 同意前不得申请任何权限;
|
||||
// 首次安装此处跳过,用户点"同意并继续"后由 Index.initPermissionAndLoad() 补申请。
|
||||
AppSettings.getPolicyAgreed(this.context)
|
||||
.then((agreed: boolean): Promise<void> => {
|
||||
if (!agreed) {
|
||||
LogUtil.write('未同意隐私政策:跳过通知权限申请');
|
||||
return Promise.resolve();
|
||||
}
|
||||
return notificationManager.isNotificationEnabled()
|
||||
.then((enabled: boolean): Promise<void> => {
|
||||
if (!enabled) {
|
||||
return notificationManager.requestEnableNotification(this.context).catch((): void => {});
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
})
|
||||
.catch((): void => {});
|
||||
// ⚠️ 这里**不再申请任何权限**(含通知权限)。
|
||||
// 依据《审核指南》7.17:应用不得在用户未主动点击对应功能时提前弹窗申请权限。
|
||||
// 通知权限改为在用户主动操作时申请:
|
||||
// ① 保存带提醒的日程(EventEditPage)② 开启「后台持续同步」(SettingsPage)
|
||||
// ③ 设置页「通知权限」→「去开启通知」按钮
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
|
||||
@@ -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)移除,故破坏性操作前必须显式确认,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user