feat(background): implement user consent mechanism for auto-start functionality
Add auto-start consent system that requires explicit user permission before enabling background sync services. Users can now choose between silent mode (no auto-start) or background mode (15-minute sync intervals). - Add consent service with AutoStartMode enum and storage utilities - Create withRemoveAutoStart Expo plugin to disable Android auto-start defaults - Integrate consent checks into JPush service, background task manager, and foreground service - Add auto-start consent UI in notification settings with descriptive dialog - Update privacy policy with section 8 explaining auto-start scenarios and user controls - Change default sync mode from BATTERY_SAVER to DISABLED - Export reinitBackgroundService function for re-initializing after consent changes - Merge OTA Android and iOS workflows into matrix build - Add release signing config in withSigning plugin for Android - Expand .dockerignore with native credential and build artifact exclusions
This commit is contained in:
@@ -12,25 +12,34 @@
|
||||
import { AppState, AppStateStatus, Platform } from 'react-native';
|
||||
import { ForegroundServiceModule } from './ForegroundServiceModule';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import {
|
||||
loadAutoStartConsent,
|
||||
saveAutoStartConsent,
|
||||
AutoStartMode,
|
||||
} from '../consent/autoStartConsent';
|
||||
|
||||
/**
|
||||
* 后台同步模式
|
||||
* 整合自启动同意机制:
|
||||
* - DISABLED(静默模式):不自启动,不注册后台任务
|
||||
* - BATTERY_SAVER(后台模式):用户同意自启动后,使用 WorkManager 后台任务(15分钟间隔)
|
||||
* - REALTIME(实时模式):用户同意自启动后,使用前台服务保活
|
||||
*/
|
||||
export enum BackgroundSyncMode {
|
||||
/** 省电模式:仅依赖 JPush 推送 */
|
||||
BATTERY_SAVER = 'battery_saver',
|
||||
|
||||
/** 实时模式:前台服务保活(仅用于通话) */
|
||||
REALTIME = 'realtime',
|
||||
|
||||
/** 禁用后台同步 */
|
||||
/** 静默模式:不自启动,仅在使用时接收消息 */
|
||||
DISABLED = 'disabled',
|
||||
|
||||
/** 后台模式:用户同意自启动,使用 WorkManager 后台任务 */
|
||||
BATTERY_SAVER = 'battery_saver',
|
||||
|
||||
/** 实时模式:用户同意自启动,前台服务保活 */
|
||||
REALTIME = 'realtime',
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'background_sync_mode';
|
||||
|
||||
class BackgroundSyncManager {
|
||||
private mode: BackgroundSyncMode = BackgroundSyncMode.BATTERY_SAVER;
|
||||
private mode: BackgroundSyncMode = BackgroundSyncMode.DISABLED;
|
||||
private appStateSubscription: ReturnType<typeof AppState.addEventListener> | null = null;
|
||||
private isInitialized: boolean = false;
|
||||
private lastSyncAt: number = 0;
|
||||
@@ -64,8 +73,18 @@ class BackgroundSyncManager {
|
||||
|
||||
/**
|
||||
* 切换后台同步模式
|
||||
* 需要用户同意自启动才能切换到 BATTERY_SAVER 或 REALTIME 模式
|
||||
*/
|
||||
async setMode(mode: BackgroundSyncMode): Promise<void> {
|
||||
// 如果要切换到需要自启动的模式,先检查用户是否同意
|
||||
if (mode !== BackgroundSyncMode.DISABLED) {
|
||||
const consent = await loadAutoStartConsent();
|
||||
if (!consent.consented || consent.mode !== AutoStartMode.BACKGROUND) {
|
||||
console.log('[BackgroundSyncManager] 用户未同意自启动,无法切换到模式:', mode);
|
||||
throw new Error('用户未同意自启动');
|
||||
}
|
||||
}
|
||||
|
||||
if (AppState.currentState !== 'active' && this.mode === BackgroundSyncMode.REALTIME) {
|
||||
await ForegroundServiceModule.stop();
|
||||
}
|
||||
@@ -161,9 +180,9 @@ class BackgroundSyncManager {
|
||||
private async loadMode(): Promise<BackgroundSyncMode> {
|
||||
try {
|
||||
const saved = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
return (saved as BackgroundSyncMode) || BackgroundSyncMode.BATTERY_SAVER;
|
||||
return (saved as BackgroundSyncMode) || BackgroundSyncMode.DISABLED;
|
||||
} catch (error) {
|
||||
return BackgroundSyncMode.BATTERY_SAVER;
|
||||
return BackgroundSyncMode.DISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
} from './BackgroundSyncManager';
|
||||
import { messageService } from '../message/messageService';
|
||||
import { api } from '../core/api';
|
||||
import {
|
||||
loadAutoStartConsent,
|
||||
isAutoStartAllowed,
|
||||
AutoStartMode,
|
||||
} from '../consent/autoStartConsent';
|
||||
|
||||
// 后台任务名称
|
||||
const BACKGROUND_SYNC_TASK = 'background-sync-task';
|
||||
@@ -36,6 +41,13 @@ TaskManager.defineTask(BACKGROUND_SYNC_TASK, async () => {
|
||||
return BackgroundTask.BackgroundTaskResult.Success;
|
||||
}
|
||||
|
||||
// 检查用户是否同意自启动
|
||||
const consent = await loadAutoStartConsent();
|
||||
if (!consent.consented || consent.mode !== AutoStartMode.BACKGROUND) {
|
||||
console.log('[BackgroundService] 用户未同意自启动,跳过后台同步');
|
||||
return BackgroundTask.BackgroundTaskResult.Success;
|
||||
}
|
||||
|
||||
// 执行同步
|
||||
await syncMessages();
|
||||
|
||||
@@ -82,6 +94,7 @@ async function syncMessages(): Promise<void> {
|
||||
|
||||
/**
|
||||
* 注册后台任务(expo-background-task)
|
||||
* 仅在用户同意自启动且模式为 BATTERY_SAVER 时注册
|
||||
*/
|
||||
async function registerBackgroundTask(): Promise<void> {
|
||||
if (Platform.OS === 'web') {
|
||||
@@ -89,6 +102,13 @@ async function registerBackgroundTask(): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查用户是否同意自启动
|
||||
const consent = await loadAutoStartConsent();
|
||||
if (!consent.consented || consent.mode !== AutoStartMode.BACKGROUND) {
|
||||
console.log('[BackgroundService] 用户未同意自启动,跳过注册后台任务');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查后台任务状态
|
||||
const status = await BackgroundTask.getStatusAsync();
|
||||
if (status !== BackgroundTask.BackgroundTaskStatus.Available) {
|
||||
@@ -129,7 +149,7 @@ async function unregisterBackgroundTask(): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化后台保活服务
|
||||
* 根据用户同意的自启动模式初始化后台服务
|
||||
*/
|
||||
export async function initBackgroundService(): Promise<boolean> {
|
||||
if (isInitialized) {
|
||||
@@ -142,14 +162,21 @@ export async function initBackgroundService(): Promise<boolean> {
|
||||
}
|
||||
|
||||
try {
|
||||
// 加载用户同意状态
|
||||
await loadAutoStartConsent();
|
||||
|
||||
// 设置同步回调
|
||||
backgroundSyncManager.setSyncMessagesCallback(syncMessages);
|
||||
|
||||
// 初始化后台同步管理器
|
||||
await backgroundSyncManager.initialize();
|
||||
|
||||
// 注册 expo-background-task 任务(用于 BATTERY_SAVER 模式)
|
||||
await registerBackgroundTask();
|
||||
// 仅在用户同意自启动时注册后台任务
|
||||
if (isAutoStartAllowed()) {
|
||||
await registerBackgroundTask();
|
||||
} else {
|
||||
console.log('[BackgroundService] 用户选择静默模式,不注册后台自启动任务');
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
console.log('[BackgroundService] 初始化完成');
|
||||
@@ -160,6 +187,39 @@ export async function initBackgroundService(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新初始化后台服务(在用户更改自启动同意后调用)
|
||||
*/
|
||||
export async function reinitBackgroundService(): Promise<boolean> {
|
||||
if (!isInitialized) {
|
||||
return initBackgroundService();
|
||||
}
|
||||
|
||||
try {
|
||||
const consent = await loadAutoStartConsent();
|
||||
|
||||
if (consent.consented && consent.mode === AutoStartMode.BACKGROUND) {
|
||||
// 用户同意自启动,注册后台任务
|
||||
await registerBackgroundTask();
|
||||
// 如果模式是实时模式,启动前台服务
|
||||
if (backgroundSyncManager.getMode() === BackgroundSyncMode.REALTIME) {
|
||||
await backgroundSyncManager.setMode(BackgroundSyncMode.REALTIME);
|
||||
}
|
||||
} else {
|
||||
// 用户拒绝自启动,取消后台任务
|
||||
await unregisterBackgroundTask();
|
||||
// 停止前台服务
|
||||
await backgroundSyncManager.setMode(BackgroundSyncMode.DISABLED);
|
||||
}
|
||||
|
||||
console.log('[BackgroundService] 根据用户同意状态重新初始化完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[BackgroundService] 重新初始化失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止后台保活服务
|
||||
*/
|
||||
@@ -205,6 +265,7 @@ export async function checkBackgroundStatus(): Promise<{
|
||||
|
||||
/**
|
||||
* 设置后台同步模式
|
||||
* 同时更新自启动同意状态
|
||||
*/
|
||||
export async function setBackgroundSyncMode(mode: BackgroundSyncMode): Promise<void> {
|
||||
await backgroundSyncManager.setMode(mode);
|
||||
@@ -239,6 +300,7 @@ export { BackgroundSyncMode };
|
||||
// 后台服务实例
|
||||
export const backgroundService = {
|
||||
init: initBackgroundService,
|
||||
reinit: reinitBackgroundService,
|
||||
stop: stopBackgroundService,
|
||||
setMode: setBackgroundSyncMode,
|
||||
getMode: getBackgroundSyncMode,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
backgroundService,
|
||||
initBackgroundService,
|
||||
reinitBackgroundService,
|
||||
stopBackgroundService,
|
||||
triggerVibration,
|
||||
vibrateOnMessage,
|
||||
|
||||
201
src/services/consent/autoStartConsent.ts
Normal file
201
src/services/consent/autoStartConsent.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
/**
|
||||
* 自启动权限同意管理
|
||||
*
|
||||
* 管理用户对应用自启动/关联启动行为的同意状态
|
||||
* 符合隐私合规要求:未经用户同意不得进行自启动
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = 'auto_start_consent';
|
||||
const STORAGE_KEY_FIRST_LAUNCH = 'auto_start_first_launch';
|
||||
|
||||
export enum AutoStartMode {
|
||||
/** 静默模式:不自启动,仅在使用时接收消息 */
|
||||
SILENT = 'silent',
|
||||
/** 后台模式:允许自启动以接收实时推送 */
|
||||
BACKGROUND = 'background',
|
||||
}
|
||||
|
||||
export interface AutoStartConsent {
|
||||
/** 用户是否同意自启动 */
|
||||
consented: boolean;
|
||||
/** 当前模式 */
|
||||
mode: AutoStartMode;
|
||||
/** 同意时间 */
|
||||
consentedAt?: string;
|
||||
/** 用户同意的目的描述 */
|
||||
purpose?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_CONSENT: AutoStartConsent = {
|
||||
consented: false,
|
||||
mode: AutoStartMode.SILENT,
|
||||
};
|
||||
|
||||
let cachedConsent: AutoStartConsent = { ...DEFAULT_CONSENT };
|
||||
|
||||
/**
|
||||
* 加载自启动同意状态
|
||||
*/
|
||||
export async function loadAutoStartConsent(): Promise<AutoStartConsent> {
|
||||
if (Platform.OS === 'web') {
|
||||
return { ...DEFAULT_CONSENT };
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
cachedConsent = JSON.parse(raw) as AutoStartConsent;
|
||||
return { ...cachedConsent };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AutoStartConsent] 加载同意状态失败:', error);
|
||||
}
|
||||
|
||||
return { ...DEFAULT_CONSENT };
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存自启动同意状态
|
||||
*/
|
||||
export async function saveAutoStartConsent(consent: AutoStartConsent): Promise<void> {
|
||||
if (Platform.OS === 'web') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cachedConsent = { ...consent };
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(consent));
|
||||
} catch (error) {
|
||||
console.error('[AutoStartConsent] 保存同意状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同步的自启动同意状态
|
||||
*/
|
||||
export function getAutoStartConsentSync(): AutoStartConsent {
|
||||
return { ...cachedConsent };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是首次启动(用于显示首次同意弹窗)
|
||||
*/
|
||||
export async function isFirstLaunch(): Promise<boolean> {
|
||||
if (Platform.OS === 'web') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(STORAGE_KEY_FIRST_LAUNCH);
|
||||
if (raw === null) {
|
||||
await AsyncStorage.setItem(STORAGE_KEY_FIRST_LAUNCH, 'false');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户同意自启动(后台模式)
|
||||
* @param purpose 用户同意的目的描述
|
||||
*/
|
||||
export async function consentToAutoStart(purpose: string = '接收实时消息推送'): Promise<void> {
|
||||
await saveAutoStartConsent({
|
||||
consented: true,
|
||||
mode: AutoStartMode.BACKGROUND,
|
||||
consentedAt: new Date().toISOString(),
|
||||
purpose,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户拒绝自启动(静默模式)
|
||||
*/
|
||||
export async function rejectAutoStart(): Promise<void> {
|
||||
await saveAutoStartConsent({
|
||||
consented: false,
|
||||
mode: AutoStartMode.SILENT,
|
||||
consentedAt: new Date().toISOString(),
|
||||
purpose: '用户拒绝自启动',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换自启动模式
|
||||
*/
|
||||
export async function setAutoStartMode(mode: AutoStartMode): Promise<void> {
|
||||
const current = getAutoStartConsentSync();
|
||||
|
||||
if (mode === AutoStartMode.BACKGROUND) {
|
||||
await saveAutoStartConsent({
|
||||
consented: true,
|
||||
mode: AutoStartMode.BACKGROUND,
|
||||
consentedAt: new Date().toISOString(),
|
||||
purpose: current.purpose || '接收实时消息推送',
|
||||
});
|
||||
} else {
|
||||
await saveAutoStartConsent({
|
||||
consented: false,
|
||||
mode: AutoStartMode.SILENT,
|
||||
consentedAt: new Date().toISOString(),
|
||||
purpose: '用户选择静默模式',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否允许自启动
|
||||
*/
|
||||
export function isAutoStartAllowed(): boolean {
|
||||
return cachedConsent.consented && cachedConsent.mode === AutoStartMode.BACKGROUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前模式
|
||||
*/
|
||||
export function getCurrentAutoStartMode(): AutoStartMode {
|
||||
return cachedConsent.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置同意状态(用于测试或用户撤销同意)
|
||||
*/
|
||||
export async function resetAutoStartConsent(): Promise<void> {
|
||||
cachedConsent = { ...DEFAULT_CONSENT };
|
||||
await AsyncStorage.removeItem(STORAGE_KEY);
|
||||
await AsyncStorage.removeItem(STORAGE_KEY_FIRST_LAUNCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自启动说明文本(用于隐私政策和弹窗)
|
||||
*/
|
||||
export function getAutoStartDescription(): string {
|
||||
return `为了及时接收消息推送,应用需要在以下场景自启动:
|
||||
|
||||
1. 设备开机完成后:恢复后台消息推送服务
|
||||
2. 应用更新后:恢复后台任务调度
|
||||
3. 系统重启后:恢复通知服务
|
||||
|
||||
自启动行为仅用于消息推送,不会收集额外个人信息。
|
||||
|
||||
您可以在"设置-通知设置"中随时更改此选项。`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自启动目的说明(用于隐私政策)
|
||||
*/
|
||||
export function getAutoStartPurposeForPrivacy(): string {
|
||||
return `应用自启动/关联启动的目的与规则:
|
||||
|
||||
• 目的:确保用户能够及时接收消息推送通知
|
||||
• 场景:设备开机、应用更新、系统重启后
|
||||
• 规则:仅在用户同意后才启用自启动功能
|
||||
• 必要性:对于需要实时消息通知的用户是必要的
|
||||
• 用户控制:用户可随时在设置中关闭此功能
|
||||
• 关闭影响:关闭后需手动打开应用才能接收消息`;
|
||||
}
|
||||
16
src/services/consent/index.ts
Normal file
16
src/services/consent/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export {
|
||||
AutoStartMode,
|
||||
loadAutoStartConsent,
|
||||
saveAutoStartConsent,
|
||||
getAutoStartConsentSync,
|
||||
isFirstLaunch,
|
||||
consentToAutoStart,
|
||||
rejectAutoStart,
|
||||
setAutoStartMode,
|
||||
isAutoStartAllowed,
|
||||
getCurrentAutoStartMode,
|
||||
resetAutoStartConsent,
|
||||
getAutoStartDescription,
|
||||
getAutoStartPurposeForPrivacy,
|
||||
} from './autoStartConsent';
|
||||
export type { AutoStartConsent } from './autoStartConsent';
|
||||
@@ -112,8 +112,10 @@ class JPushService {
|
||||
// Register listeners only once (idempotent guard)
|
||||
this._registerListeners();
|
||||
|
||||
// 退后台保持极光长连接,确保推送实时到达
|
||||
this._setBackgroundKeepLongConn(true);
|
||||
// 检查用户是否同意自启动,仅在同意后才保持后台长连接
|
||||
const { isAutoStartAllowed } = await import('../consent/autoStartConsent');
|
||||
const keepLongConn = isAutoStartAllowed();
|
||||
this._setBackgroundKeepLongConn(keepLongConn);
|
||||
|
||||
// 初始化 SDK
|
||||
JPush!.init({
|
||||
|
||||
Reference in New Issue
Block a user