From f3d54a3f4cc6fe3251dff27aa896361396df3b07 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Mon, 15 Jun 2026 20:43:15 +0800 Subject: [PATCH] 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 --- .dockerignore | 32 ++- .gitea/workflows/build.yml | 184 ++++------------ .npmrc | 5 + app.json | 1 + app/_layout.tsx | 1 + plugins/withRemoveAutoStart.js | 87 ++++++++ plugins/withSigning.js | 90 +++++++- src/hooks/useRegisterPushDevice.ts | 7 + .../profile/NotificationSettingsScreen.tsx | 119 +++++++++-- src/screens/profile/PrivacyPolicyScreen.tsx | 62 +++++- .../background/BackgroundSyncManager.ts | 39 +++- src/services/background/backgroundService.ts | 68 +++++- src/services/background/index.ts | 1 + src/services/consent/autoStartConsent.ts | 201 ++++++++++++++++++ src/services/consent/index.ts | 16 ++ src/services/notification/jpushService.ts | 6 +- 16 files changed, 746 insertions(+), 173 deletions(-) create mode 100644 .npmrc create mode 100644 plugins/withRemoveAutoStart.js create mode 100644 src/services/consent/autoStartConsent.ts create mode 100644 src/services/consent/index.ts diff --git a/.dockerignore b/.dockerignore index eb1e6ac..1595775 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,40 @@ +# Dependencies node_modules + +# Source control .git +.gitignore + +# Expo caches .expo +.expo-shared + +# Build artifacts dist dist-web dist-android +dist-ios dist-android-update.zip -android +dist-ios-update.zip +android +ios +web-build + +# Native credentials (never ship into image) +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* + +# Misc screenshots *.log +.vscode +.idea +.DS_Store +*.swp +*.swo +coverage +__tests__ diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index dad1b0e..73fa9af 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -21,17 +21,18 @@ on: env: REGISTRY: code.littlelan.cn IMAGE_NAME: carrot_bbs/frontend-web - OTA_PLATFORM_ANDROID: android - OTA_PLATFORM_IOS: ios OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest jobs: - ota-android: + ota: runs-on: ubuntu-latest if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_ota == 'true') permissions: contents: read + strategy: + matrix: + platform: [android, ios] steps: - name: Checkout code uses: actions/checkout@v4 @@ -41,12 +42,7 @@ jobs: with: node-version: '25.6.1' registry-url: 'https://registry.npmmirror.com' - - - name: Remove deprecated always-auth npm config - run: | - if [ -f ~/.npmrc ]; then - sed -i '/always-auth/d' ~/.npmrc - fi + cache: 'npm' - name: Install dependencies run: npm ci @@ -58,117 +54,44 @@ jobs: echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT" echo "Resolved runtimeVersion: ${RUNTIME_VERSION}" - - name: Export Android update bundle + - name: Export update bundle run: | - rm -rf dist-android dist-android-update.zip - npx expo export --platform android --output-dir dist-android - npx expo config --type public --json > dist-android/expoConfig.json + rm -rf dist-${{ matrix.platform }} dist-${{ matrix.platform }}-update.zip + npx expo export --platform ${{ matrix.platform }} --output-dir dist-${{ matrix.platform }} + npx expo config --type public --json > dist-${{ matrix.platform }}/expoConfig.json - - name: Archive Android update bundle + - name: Archive update bundle run: | - python - <<'PY' - import os - import zipfile - - with zipfile.ZipFile('dist-android-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf: - for root, _, files in os.walk('dist-android'): + python -c " + import os, zipfile + p = '${{ matrix.platform }}' + dist = f'dist-{p}' + with zipfile.ZipFile(f'dist-{p}-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf: + for root, _, files in os.walk(dist): for name in files: src = os.path.join(root, name) - arc = os.path.relpath(src, 'dist-android') + arc = os.path.relpath(src, dist) zf.write(src, arc) - PY + " - name: Publish OTA env: OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }} run: | test -n "${OTA_AUTH_TOKEN}" || (echo "Missing secret OTA_AUTH_TOKEN" && exit 1) - curl -fSs -X POST "${OTA_PUBLISH_URL}?runtimeVersion=${{ steps.runtime.outputs.runtime_version }}&platform=${OTA_PLATFORM_ANDROID}" \ + curl -fSs -X POST "${OTA_PUBLISH_URL}?runtimeVersion=${{ steps.runtime.outputs.runtime_version }}&platform=${{ matrix.platform }}" \ -H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \ -H "Content-Type: application/zip" \ - --data-binary @"dist-android-update.zip" + --data-binary @"dist-${{ matrix.platform }}-update.zip" - name: Verify OTA manifest run: | REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \ - -H "expo-platform: ${OTA_PLATFORM_ANDROID}" \ - -H "expo-runtime-version: ${{ steps.runtime.outputs.runtime_version }}" \ - -H "expo-protocol-version: 1" \ - | python -c "import sys, json, re; s=sys.stdin.read(); m=re.search(r'\\{\\\"id\\\":.*\\\"extra\\\":\\{.*\\}\\}', s); j=json.loads(m.group(0)); print(j['launchAsset']['url'].split('/')[-1].split('&')[0])")" - LOCAL="$(python -c "import json; m=json.load(open('dist-android/metadata.json')); print(m['fileMetadata']['android']['bundle'].split('/')[-1])")" - echo "Remote bundle: ${REMOTE}" - echo "Local bundle: ${LOCAL}" - test "${REMOTE}" = "${LOCAL}" - - ota-ios: - runs-on: ubuntu-latest - if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_ota == 'true') - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: '25.6.1' - registry-url: 'https://registry.npmmirror.com' - - - name: Remove deprecated always-auth npm config - run: | - if [ -f ~/.npmrc ]; then - sed -i '/always-auth/d' ~/.npmrc - fi - - - name: Install dependencies - run: npm ci - - - name: Resolve runtime version - id: runtime - run: | - RUNTIME_VERSION="$(node -p "require('./app.json').expo.version")" - echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT" - echo "Resolved runtimeVersion: ${RUNTIME_VERSION}" - - - name: Export iOS update bundle - run: | - rm -rf dist-ios dist-ios-update.zip - npx expo export --platform ios --output-dir dist-ios - npx expo config --type public --json > dist-ios/expoConfig.json - - - name: Archive iOS update bundle - run: | - python - <<'PY' - import os - import zipfile - - with zipfile.ZipFile('dist-ios-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf: - for root, _, files in os.walk('dist-ios'): - for name in files: - src = os.path.join(root, name) - arc = os.path.relpath(src, 'dist-ios') - zf.write(src, arc) - PY - - - name: Publish OTA - env: - OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }} - run: | - test -n "${OTA_AUTH_TOKEN}" || (echo "Missing secret OTA_AUTH_TOKEN" && exit 1) - curl -fSs -X POST "${OTA_PUBLISH_URL}?runtimeVersion=${{ steps.runtime.outputs.runtime_version }}&platform=${OTA_PLATFORM_IOS}" \ - -H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \ - -H "Content-Type: application/zip" \ - --data-binary @"dist-ios-update.zip" - - - name: Verify OTA manifest - run: | - REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \ - -H "expo-platform: ${OTA_PLATFORM_IOS}" \ + -H "expo-platform: ${{ matrix.platform }}" \ -H "expo-runtime-version: ${{ steps.runtime.outputs.runtime_version }}" \ -H "expo-protocol-version: 1" \ | python -c "import sys, json, re; s=sys.stdin.read(); m=re.search(r'\\{\"id\":.*\"extra\":\{.*\}\}', s); j=json.loads(m.group(0)); print(j['launchAsset']['url'].split('/')[-1].split('&')[0])")" - LOCAL="$(python -c "import json; m=json.load(open('dist-ios/metadata.json')); print(m['fileMetadata']['ios']['bundle'].split('/')[-1])")" + LOCAL="$(python -c "import json; m=json.load(open('dist-${{ matrix.platform }}/metadata.json')); print(m['fileMetadata']['${{ matrix.platform }}']['bundle'].split('/')[-1])")" echo "Remote bundle: ${REMOTE}" echo "Local bundle: ${LOCAL}" test "${REMOTE}" = "${LOCAL}" @@ -202,27 +125,25 @@ jobs: with: node-version: '25.6.1' registry-url: 'https://registry.npmmirror.com' + cache: 'npm' - - name: Remove deprecated always-auth npm config - run: | - if [ -f ~/.npmrc ]; then - sed -i '/always-auth/d' ~/.npmrc - fi + - name: Install dependencies + run: npm ci - name: Cache Android NDK uses: actions/cache@v4 id: cache-ndk with: - path: /opt/android/ndk/27.1.12297006 - key: ndk-27.1.12297006-v1 + path: /opt/android/ndk + key: ndk-v2 - name: Install Android NDK if: steps.cache-ndk.outputs.cache-hit != 'true' run: | echo "Existing NDK versions:" ls /opt/android/ndk/ 2>/dev/null || echo "No NDK dir" - echo "Installing NDK 27.1.12297006..." - yes | sdkmanager --install "ndk;27.1.12297006" + echo "Installing required NDK versions..." + yes | sdkmanager --install "ndk;27.1.12297006" "ndk;27.0.12077973" echo "NDK after install:" ls /opt/android/ndk/ @@ -233,29 +154,19 @@ jobs: ~/.gradle/caches ~/.gradle/wrapper ~/.android/build-cache - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/gradle.properties') }} + key: ${{ runner.os }}-gradle-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-gradle- - - name: Cache node_modules - uses: actions/cache@v4 - id: cache-node-modules - with: - path: node_modules - key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-modules- - - - name: Install dependencies - if: steps.cache-node-modules.outputs.cache-hit != 'true' - run: npm ci - - name: Generate Android native project run: npx expo prebuild --platform android - - name: Switch Gradle distribution to Tencent mirror + - name: Configure Gradle with China mirrors and signing run: | - PROPS="android/gradle/wrapper/gradle-wrapper.properties" + cd android + + # Switch Gradle distribution to Tencent mirror + PROPS="gradle/wrapper/gradle-wrapper.properties" if [ -f "$PROPS" ]; then sed -i 's|distributionUrl=https\\://services.gradle.org/distributions/|distributionUrl=https\\://mirrors.cloud.tencent.com/gradle/|' "$PROPS" echo "Updated gradle-wrapper.properties:" @@ -264,14 +175,9 @@ jobs: echo "gradle-wrapper.properties not found" exit 1 fi - - - name: Decode Android signing keystore - run: | - echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/withyou-release-key.keystore - - - name: Configure Gradle with China Maven mirrors - run: | - cd android + + # Decode Android signing keystore + echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > app/withyou-release-key.keystore # Update settings.gradle with China Maven mirrors cat > settings.gradle << 'SETTINGS_EOF' @@ -368,7 +274,7 @@ jobs: apply plugin: "com.facebook.react.rootproject" BUILD_EOF - # Update gradle.properties + # Update gradle.properties (without secrets for better cache hit rate) cat > gradle.properties << 'PROPS_EOF' org.gradle.daemon=false org.gradle.parallel=true @@ -387,16 +293,18 @@ jobs: ndkVersion=27.1.12297006 expo.useLegacyPackaging=false PROPS_EOF - - - name: Configure Gradle signing properties - run: | - cd android + + # Append signing properties (secrets appended, not cached) cat >> gradle.properties << SIGNING_PROPS MYAPP_UPLOAD_STORE_FILE=withyou-release-key.keystore MYAPP_UPLOAD_STORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} MYAPP_UPLOAD_KEY_ALIAS=withyou-key MYAPP_UPLOAD_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }} SIGNING_PROPS + + # Verify signing config in app/build.gradle + echo "=== app/build.gradle signing section ===" + grep -A 20 'signingConfigs' app/build.gradle || echo "signingConfigs not found" - name: Build Android release APK (arm64 only) run: | @@ -405,7 +313,7 @@ jobs: ./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --max-workers=4 --parallel - name: Upload APK artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: withyou-android-release-apk path: android/app/build/outputs/apk/release/app-release.apk diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..58fd2d6 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# Use China npm mirror for faster downloads in CI +registry=https://registry.npmmirror.com + +# Disable always-auth to avoid deprecation warnings +always-auth=false diff --git a/app.json b/app.json index c633dce..5a85713 100644 --- a/app.json +++ b/app.json @@ -100,6 +100,7 @@ "minimumInterval": 15 } ], + "./plugins/withRemoveAutoStart", [ "expo-media-library", { diff --git a/app/_layout.tsx b/app/_layout.tsx index a7b5fc0..3d913cf 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -146,6 +146,7 @@ function NotificationBootstrap() { const { systemNotificationService } = await import('@/services/notification'); await systemNotificationService.initialize(); const { initBackgroundService } = await import('@/services/background'); + // 默认静默模式,不会注册后台任务,不会触发自启动 await initBackgroundService(); const subscription = AppState.addEventListener('change', (nextAppState) => { diff --git a/plugins/withRemoveAutoStart.js b/plugins/withRemoveAutoStart.js new file mode 100644 index 0000000..584e577 --- /dev/null +++ b/plugins/withRemoveAutoStart.js @@ -0,0 +1,87 @@ +/** + * withRemoveAutoStart — 移除应用退出后的自启动/关联启动行为 + * + * 整改 2.2.6 APP频繁自启动和关联启动(存在风险) + * + * 问题:以下 SDK 注册了 BOOT_COMPLETED 等 intent-filter, + * 导致设备重启时应用被自动唤醒: + * - expo.modules.taskManager.TaskBroadcastReceiver + * - expo.modules.notifications.service.NotificationsService + * - androidx.work.impl.background.systemalarm.RescheduleReceiver + * - androidx.work.impl.background.systemalarm.ConstraintProxy* (多个) + * + * 本插件在 final manifest 合并阶段移除这些 intent-filter action, + * 从根本上消除应用退出后的自启动行为。 + */ + +const { withAndroidManifest } = require('expo/config-plugins'); + +// 需要从 receivers 中移除的自启动相关 action +const AUTO_START_ACTIONS = new Set([ + 'android.intent.action.BOOT_COMPLETED', + 'android.intent.action.REBOOT', + 'android.intent.action.QUICKBOOT_POWERON', + 'com.htc.intent.action.QUICKBOOT_POWERON', +]); + +function removeAutoStartActionsFromManifest(androidManifest) { + const app = androidManifest.manifest.application?.[0]; + if (!app) return androidManifest; + + // 处理 标签 + const receivers = app.receiver || []; + for (const receiver of receivers) { + if (!receiver['intent-filter']) continue; + + receiver['intent-filter'] = receiver['intent-filter'].map((filter) => { + if (!filter.action) return filter; + + const originalCount = filter.action.length; + filter.action = filter.action.filter( + (action) => !AUTO_START_ACTIONS.has(action.$?.['android:name']) + ); + + if (filter.action.length !== originalCount) { + const removed = originalCount - filter.action.length; + console.log( + `[withRemoveAutoStart] 移除了 ${removed} 个自启动 action (receiver)` + ); + } + + return filter; + }); + + // 如果 intent-filter 中没有 action 了,移除整个 intent-filter + receiver['intent-filter'] = receiver['intent-filter'].filter( + (filter) => filter.action && filter.action.length > 0 + ); + } + + // 移除 RECEIVE_BOOT_COMPLETED 权限声明 + if (androidManifest.manifest['uses-permission']) { + const originalPerms = androidManifest.manifest['uses-permission'].length; + androidManifest.manifest['uses-permission'] = androidManifest.manifest[ + 'uses-permission' + ].filter((perm) => { + const name = perm.$?.['android:name']; + return name !== 'android.permission.RECEIVE_BOOT_COMPLETED'; + }); + const removedPerms = originalPerms - androidManifest.manifest['uses-permission'].length; + if (removedPerms > 0) { + console.log( + `[withRemoveAutoStart] 移除了 RECEIVE_BOOT_COMPLETED 权限声明 (${removedPerms} 处)` + ); + } + } + + return androidManifest; +} + +function withRemoveAutoStart(config) { + return withAndroidManifest(config, (config) => { + config.modResults = removeAutoStartActionsFromManifest(config.modResults); + return config; + }); +} + +module.exports = withRemoveAutoStart; diff --git a/plugins/withSigning.js b/plugins/withSigning.js index 56ae607..4c779bc 100644 --- a/plugins/withSigning.js +++ b/plugins/withSigning.js @@ -35,7 +35,95 @@ const withSigning = (config, options = {}) => { }, ]); + config = withDangerousMod(config, [ + 'android', + async (config) => { + const platformRoot = config.modRequest.platformProjectRoot; + const buildGradlePath = path.join(platformRoot, 'app', 'build.gradle'); + + if (!fs.existsSync(buildGradlePath)) return config; + + let lines = fs.readFileSync(buildGradlePath, 'utf-8').split('\n'); + + // 1. Add release signing config that reads from gradle.properties + const hasReleaseSigning = lines.some((l) => l.includes('MYAPP_UPLOAD_STORE_FILE')); + if (!hasReleaseSigning) { + // Find the debug signingConfig closing brace and insert release after it + let debugBraceLine = -1; + let inSigningConfigs = false; + let braceDepth = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes('signingConfigs {') || line.match(/^\s*signingConfigs\s*\{/)) { + inSigningConfigs = true; + braceDepth = 1; + continue; + } + if (inSigningConfigs) { + braceDepth += (line.match(/\{/g) || []).length; + braceDepth -= (line.match(/\}/g) || []).length; + if (braceDepth === 0) { + debugBraceLine = i; // This is the closing } of signingConfigs + break; + } + } + } + + if (debugBraceLine > 0) { + const releaseBlock = [ + ' release {', + " if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {", + ' storeFile file(MYAPP_UPLOAD_STORE_FILE)', + ' storePassword MYAPP_UPLOAD_STORE_PASSWORD', + ' keyAlias MYAPP_UPLOAD_KEY_ALIAS', + ' keyPassword MYAPP_UPLOAD_KEY_PASSWORD', + ' }', + ' }', + ]; + lines.splice(debugBraceLine, 0, ...releaseBlock); + } + } + + // 2. Change release buildType to use release signingConfig + // Walk through buildTypes > release and replace signingConfigs.debug -> signingConfigs.release + let inBuildTypes = false; + let inRelease = false; + let buildTypesDepth = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes('buildTypes {') || line.match(/^\s*buildTypes\s*\{/)) { + inBuildTypes = true; + buildTypesDepth = 1; + continue; + } + if (inBuildTypes) { + buildTypesDepth += (line.match(/\{/g) || []).length; + buildTypesDepth -= (line.match(/\}/g) || []).length; + + if (line.match(/^\s*release\s*\{/)) { + inRelease = true; + } + + if (inRelease && line.includes('signingConfig signingConfigs.debug')) { + lines[i] = line.replace('signingConfig signingConfigs.debug', 'signingConfig signingConfigs.release'); + inRelease = false; + } + + if (buildTypesDepth === 0) { + inBuildTypes = false; + inRelease = false; + } + } + } + + fs.writeFileSync(buildGradlePath, lines.join('\n')); + return config; + }, + ]); + return config; }; -module.exports = withSigning; \ No newline at end of file +module.exports = withSigning; diff --git a/src/hooks/useRegisterPushDevice.ts b/src/hooks/useRegisterPushDevice.ts index 05c8cd6..f17d703 100644 --- a/src/hooks/useRegisterPushDevice.ts +++ b/src/hooks/useRegisterPushDevice.ts @@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'; import { Platform } from 'react-native'; import { jpushService } from '../services/notification/jpushService'; +import { isAutoStartAllowed } from '../services/consent/autoStartConsent'; export function useRegisterPushDevice(isAuthenticated: boolean, userID?: string) { const deviceRegistered = useRef(false); @@ -9,6 +10,12 @@ export function useRegisterPushDevice(isAuthenticated: boolean, userID?: string) useEffect(() => { if (Platform.OS === 'web' || !isAuthenticated || !userID) return; + // 仅在用户同意自启动后才初始化 JPush + if (!isAutoStartAllowed()) { + console.log('[useRegisterPushDevice] 用户未同意自启动,跳过 JPush 初始化'); + return; + } + if (!deviceRegistered.current) { deviceRegistered.current = true; diff --git a/src/screens/profile/NotificationSettingsScreen.tsx b/src/screens/profile/NotificationSettingsScreen.tsx index d7871ea..de9431c 100644 --- a/src/screens/profile/NotificationSettingsScreen.tsx +++ b/src/screens/profile/NotificationSettingsScreen.tsx @@ -29,6 +29,12 @@ import { } from '@/services/notification'; import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { backgroundSyncManager, BackgroundSyncMode } from '@/services/background'; +import { + loadAutoStartConsent, + consentToAutoStart, + rejectAutoStart, + getAutoStartDescription, +} from '@/services/consent'; // 内容最大宽度 const CONTENT_MAX_WIDTH = 720; @@ -49,8 +55,9 @@ export const NotificationSettingsScreen: React.FC = () => { const [vibrationEnabled, setVibrationEnabledState] = useState(true); const [pushEnabled, setPushEnabled] = useState(true); const [soundEnabled, setSoundEnabled] = useState(true); - const [syncMode, setSyncMode] = useState(BackgroundSyncMode.BATTERY_SAVER); + const [syncMode, setSyncMode] = useState(BackgroundSyncMode.DISABLED); const [systemPushEnabled, setSystemPushEnabled] = useState(null); + const [autoStartConsented, setAutoStartConsented] = useState(false); const { isWideScreen, isMobile } = useResponsive(); const insets = useSafeAreaInsets(); @@ -67,6 +74,10 @@ export const NotificationSettingsScreen: React.FC = () => { setSoundEnabled(prefs.soundEnabled); setSyncMode(backgroundSyncManager.getMode()); + // 加载自启动同意状态 + const consent = await loadAutoStartConsent(); + setAutoStartConsented(consent.consented); + if (Platform.OS !== 'web') { const enabled = await jpushService.checkNotificationPermission(); setSystemPushEnabled(enabled); @@ -106,6 +117,41 @@ export const NotificationSettingsScreen: React.FC = () => { }; const handleSyncModeChange = async (mode: BackgroundSyncMode) => { + // 如果要切换到需要自启动的模式,先请求用户同意 + if (mode !== BackgroundSyncMode.DISABLED && !autoStartConsented) { + Alert.alert( + '后台消息接收', + getAutoStartDescription() + '\n\n开启后,应用可在后台接收消息推送。', + [ + { text: '取消', style: 'cancel' }, + { + text: '同意并开启', + onPress: async () => { + try { + await consentToAutoStart('接收实时消息推送'); + setAutoStartConsented(true); + // 用户同意后,继续切换模式 + await doSetSyncMode(mode); + } catch (error) { + console.error('同意自启动失败:', error); + } + }, + }, + ] + ); + return; + } + + // 如果切换到禁用模式,撤销自启动同意 + if (mode === BackgroundSyncMode.DISABLED && autoStartConsented) { + await rejectAutoStart(); + setAutoStartConsented(false); + } + + await doSetSyncMode(mode); + }; + + const doSetSyncMode = async (mode: BackgroundSyncMode) => { if (mode === BackgroundSyncMode.REALTIME) { Alert.alert( '实时模式', @@ -129,25 +175,56 @@ export const NotificationSettingsScreen: React.FC = () => { } }; + const handleAutoStartModeChange = async (mode: AutoStartMode) => { + if (mode === AutoStartMode.BACKGROUND) { + Alert.alert( + '后台消息接收', + getAutoStartDescription() + '\n\n开启后,应用可在后台接收消息推送。', + [ + { text: '取消', style: 'cancel' }, + { + text: '同意并开启', + onPress: async () => { + await setAutoStartMode(AutoStartMode.BACKGROUND); + setAutoStartModeState(AutoStartMode.BACKGROUND); + setAutoStartConsented(true); + // 重新初始化后台服务以应用新设置 + const { reinitBackgroundService } = await import('@/services/background'); + await reinitBackgroundService(); + Alert.alert('已开启', '后台消息接收已开启。您可随时在设置中关闭。'); + }, + }, + ] + ); + } else { + await setAutoStartMode(AutoStartMode.SILENT); + setAutoStartModeState(AutoStartMode.SILENT); + setAutoStartConsented(false); + // 重新初始化后台服务以应用新设置 + const { reinitBackgroundService } = await import('@/services/background'); + await reinitBackgroundService(); + } + }; + const syncModeOptions: { mode: BackgroundSyncMode; title: string; subtitle: string; icon: string }[] = [ + { + mode: BackgroundSyncMode.DISABLED, + title: '静默模式', + subtitle: '不自启动,仅在使用时接收消息', + icon: 'bell-off-outline', + }, { mode: BackgroundSyncMode.BATTERY_SAVER, - title: '省电模式', - subtitle: '系统后台任务,每 15 分钟检查一次', + title: '后台模式', + subtitle: '允许自启动,每 15 分钟检查一次', icon: 'leaf', }, { mode: BackgroundSyncMode.REALTIME, title: '实时模式', - subtitle: '通知栏常驻保活,即时同步消息', + subtitle: '允许自启动,通知栏常驻保活', icon: 'lightning-bolt', }, - { - mode: BackgroundSyncMode.DISABLED, - title: '禁用', - subtitle: '仅在应用打开时接收消息', - icon: 'close-circle-outline', - }, ]; const handleRequestSystemPermission = async () => { @@ -304,12 +381,30 @@ export const NotificationSettingsScreen: React.FC = () => { ))} - {/* 实时模式说明 */} + {/* 模式说明 */} + {syncMode === BackgroundSyncMode.DISABLED && ( + + + + 静默模式下,应用不会自启动。您仅在打开应用时接收消息。此模式最省电,但可能错过实时消息。 + + + )} + + {syncMode === BackgroundSyncMode.BATTERY_SAVER && ( + + + + 后台模式已开启,应用允许自启动以接收消息推送。系统会每 15 分钟检查一次新消息。 + + + )} + {syncMode === BackgroundSyncMode.REALTIME && ( - 实时模式已开启,应用将在通知栏显示常驻通知以保持后台运行。如需更稳定的后台运行,请在系统设置中将本应用加入电池优化白名单。 + 实时模式已开启,应用允许自启动并在通知栏显示常驻通知以保持后台运行。如需更稳定的后台运行,请在系统设置中将本应用加入电池优化白名单。 )} diff --git a/src/screens/profile/PrivacyPolicyScreen.tsx b/src/screens/profile/PrivacyPolicyScreen.tsx index 97aa167..47e10fc 100644 --- a/src/screens/profile/PrivacyPolicyScreen.tsx +++ b/src/screens/profile/PrivacyPolicyScreen.tsx @@ -30,7 +30,7 @@ const THEME_COLORS = { }; // 政策最后更新日期 -const LAST_UPDATED = '2026年4月28日'; +const LAST_UPDATED = '2026年6月15日'; // 隐私政策内容 const PRIVACY_SECTIONS = [ @@ -170,18 +170,68 @@ const PRIVACY_SECTIONS = [ • 使用目的:辅助消息推送通道管理 • 收集的个人信息:设备推送令牌(Push Token)、设备标识符 +3. WorkManager(Android系统组件) +• 提供方:Google LLC +• 使用目的:在用户同意自启动后,执行后台消息同步任务 +• 收集的个人信息:无额外个人信息收集 +• 隐私政策:https://policies.google.com/privacy + +4. expo-task-manager / expo-background-task +• 提供方:Expo(650 Industries, Inc.) +• 使用目的:在用户同意自启动后,管理后台任务调度 +• 收集的个人信息:无额外个人信息收集 + +5. expo-notifications +• 提供方:Expo(650 Industries, Inc.) +• 使用目的:在用户同意自启动后,恢复通知服务 +• 收集的个人信息:设备推送令牌 + +6. expo-callkit-telecom +• 提供方:Expo(650 Industries, Inc.) +• 使用目的:通话功能相关服务 +• 收集的个人信息:无额外个人信息收集 + 如您后续接入其他第三方SDK(如微信登录、分享等功能),我们将在本章节更新相关说明,并告知您对应SDK收集的信息类型和用途。更新后的SDK目录将在应用内公布,请以最新版本为准。`, }, { - title: '八、隐私政策的更新', - content: `8.1 我们可能会不时更新本隐私政策。更新后的隐私政策将在应用内公布,并标注更新日期。 + title: '八、应用自启动与关联启动说明', + content: `8.1 自启动/关联启动的目的与场景 +为了及时向您推送消息通知,本应用可能需要在以下场景进行自启动或关联启动: -8.2 对于重大变更,我们会在变更生效前通过应用内通知、弹窗等方式告知您。 +• 设备开机完成后:恢复后台消息推送服务,确保您能及时收到新消息提醒 +• 应用更新后:恢复后台任务调度,保证消息同步功能正常运作 +• 系统重启后:恢复通知服务,确保推送通道可用 +• 关联启动场景:当系统或其他应用触发相关事件时,为保证推送服务连续性而进行关联启动 -8.3 如您不同意变更后的内容,应立即停止使用本服务。如您继续使用本服务,即视为您已阅读并同意受变更后的隐私政策约束。`, +8.2 用户同意机制 +• 自启动/关联启动功能仅在您明确同意后才启用 +• 首次使用时,我们会在"通知设置"中向您说明自启动的目的、场景、规则及必要性,并征得您的同意 +• 您可随时在"设置-通知设置-后台同步模式"中更改选择: + - 静默模式:不自启动,仅在使用应用时接收消息 + - 后台模式:同意自启动,系统每15分钟检查一次新消息 + - 实时模式:同意自启动,通知栏常驻保活,即时同步消息 + +8.3 关闭自启动的影响 +• 选择静默模式后,应用不会自启动 +• 您仅在打开应用时才能接收消息 +• 此模式最省电,但可能错过实时消息 +• 关闭自启动不会影响应用内的其他功能使用 + +8.4 我们承诺 +• 自启动行为仅用于消息推送服务,不会用于收集额外个人信息 +• 自启动行为不会用于广告推送或其他商业目的 +• 我们仅在用户同意的范围内使用自启动功能`, }, { - title: '九、联系我们', + title: '九、隐私政策的更新', + content: `9.1 我们可能会不时更新本隐私政策。更新后的隐私政策将在应用内公布,并标注更新日期。 + +9.2 对于重大变更,我们会在变更生效前通过应用内通知、弹窗等方式告知您。 + +9.3 如您不同意变更后的内容,应立即停止使用本服务。如您继续使用本服务,即视为您已阅读并同意受变更后的隐私政策约束。`, + }, + { + title: '十、联系我们', content: `如果您对本隐私政策有任何疑问、意见或建议,或者您希望行使您的权利,请通过以下方式与我们联系: • 邮箱:system@qczlit.cn diff --git a/src/services/background/BackgroundSyncManager.ts b/src/services/background/BackgroundSyncManager.ts index 7e0b726..97b5c4f 100644 --- a/src/services/background/BackgroundSyncManager.ts +++ b/src/services/background/BackgroundSyncManager.ts @@ -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 | null = null; private isInitialized: boolean = false; private lastSyncAt: number = 0; @@ -64,8 +73,18 @@ class BackgroundSyncManager { /** * 切换后台同步模式 + * 需要用户同意自启动才能切换到 BATTERY_SAVER 或 REALTIME 模式 */ async setMode(mode: BackgroundSyncMode): Promise { + // 如果要切换到需要自启动的模式,先检查用户是否同意 + 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 { 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; } } diff --git a/src/services/background/backgroundService.ts b/src/services/background/backgroundService.ts index 40b288e..324297c 100644 --- a/src/services/background/backgroundService.ts +++ b/src/services/background/backgroundService.ts @@ -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 { /** * 注册后台任务(expo-background-task) + * 仅在用户同意自启动且模式为 BATTERY_SAVER 时注册 */ async function registerBackgroundTask(): Promise { if (Platform.OS === 'web') { @@ -89,6 +102,13 @@ async function registerBackgroundTask(): Promise { } 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 { } /** - * 初始化后台保活服务 + * 根据用户同意的自启动模式初始化后台服务 */ export async function initBackgroundService(): Promise { if (isInitialized) { @@ -142,14 +162,21 @@ export async function initBackgroundService(): Promise { } 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 { } } +/** + * 重新初始化后台服务(在用户更改自启动同意后调用) + */ +export async function reinitBackgroundService(): Promise { + 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 { await backgroundSyncManager.setMode(mode); @@ -239,6 +300,7 @@ export { BackgroundSyncMode }; // 后台服务实例 export const backgroundService = { init: initBackgroundService, + reinit: reinitBackgroundService, stop: stopBackgroundService, setMode: setBackgroundSyncMode, getMode: getBackgroundSyncMode, diff --git a/src/services/background/index.ts b/src/services/background/index.ts index 29c2a50..2aef087 100644 --- a/src/services/background/index.ts +++ b/src/services/background/index.ts @@ -1,6 +1,7 @@ export { backgroundService, initBackgroundService, + reinitBackgroundService, stopBackgroundService, triggerVibration, vibrateOnMessage, diff --git a/src/services/consent/autoStartConsent.ts b/src/services/consent/autoStartConsent.ts new file mode 100644 index 0000000..570c1fb --- /dev/null +++ b/src/services/consent/autoStartConsent.ts @@ -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 { + 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 { + 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 { + 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 { + await saveAutoStartConsent({ + consented: true, + mode: AutoStartMode.BACKGROUND, + consentedAt: new Date().toISOString(), + purpose, + }); +} + +/** + * 用户拒绝自启动(静默模式) + */ +export async function rejectAutoStart(): Promise { + await saveAutoStartConsent({ + consented: false, + mode: AutoStartMode.SILENT, + consentedAt: new Date().toISOString(), + purpose: '用户拒绝自启动', + }); +} + +/** + * 切换自启动模式 + */ +export async function setAutoStartMode(mode: AutoStartMode): Promise { + 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 { + 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 `应用自启动/关联启动的目的与规则: + +• 目的:确保用户能够及时接收消息推送通知 +• 场景:设备开机、应用更新、系统重启后 +• 规则:仅在用户同意后才启用自启动功能 +• 必要性:对于需要实时消息通知的用户是必要的 +• 用户控制:用户可随时在设置中关闭此功能 +• 关闭影响:关闭后需手动打开应用才能接收消息`; +} diff --git a/src/services/consent/index.ts b/src/services/consent/index.ts new file mode 100644 index 0000000..fe2e20e --- /dev/null +++ b/src/services/consent/index.ts @@ -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'; diff --git a/src/services/notification/jpushService.ts b/src/services/notification/jpushService.ts index c3226d3..0d659da 100644 --- a/src/services/notification/jpushService.ts +++ b/src/services/notification/jpushService.ts @@ -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({