feat(service): add foreground service for background message sync

Implement Android foreground service to keep app alive in background for real-time message sync:
- Add withForegroundService expo config plugin for notification channel setup
- Add BackgroundSyncManager with REALTIME, BATTERY_SAVER, and DISABLED modes
- Add ForegroundServiceModule for native Android foreground service binding
- Refactor backgroundService to integrate foreground service and expo-background-task

Also refactor auth and profile screens to use dynamic theme colors:
- Replace hardcoded THEME_COLORS with colors.primary.main, colors.background.paper, etc.
- Add useResolvedColorScheme for dynamic StatusBar styling
- Update NotificationSettingsScreen with sync mode selection UI

BREAKING CHANGE: Switched from expo-background-fetch to expo-background-task, minimum background sync interval changed from 900s to 15min in config
This commit is contained in:
lafay
2026-04-11 04:27:22 +08:00
parent 9bbed8cf5e
commit 6f84e17772
22 changed files with 1271 additions and 394 deletions

View File

@@ -21,13 +21,6 @@ import * as hrefs from '../../navigation/hrefs';
const CONTENT_MAX_WIDTH = 720;
// 胡萝卜橙主题色
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
function createAccountDeletionStyles(colors: AppColors) {
return StyleSheet.create({
container: {
@@ -114,7 +107,7 @@ function createAccountDeletionStyles(colors: AppColors) {
},
// 输入框 - 扁平化风格
input: {
backgroundColor: '#F5F5F7',
backgroundColor: colors.background.default,
borderRadius: 14,
padding: spacing.md,
fontSize: 16,
@@ -133,12 +126,12 @@ function createAccountDeletionStyles(colors: AppColors) {
flex: 1,
height: 56,
borderRadius: 14,
backgroundColor: THEME_COLORS.primary,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
},
primaryButtonText: {
color: '#fff',
color: colors.text.inverse,
fontSize: fontSizes.md,
fontWeight: '600',
},
@@ -166,7 +159,7 @@ function createAccountDeletionStyles(colors: AppColors) {
justifyContent: 'center',
},
dangerButtonText: {
color: '#fff',
color: colors.text.inverse,
fontSize: fontSizes.md,
fontWeight: '600',
},
@@ -392,9 +385,9 @@ export const AccountDeletionScreen: React.FC = () => {
onPress={handleRequestDeletion}
disabled={submitting || !password.trim()}
>
{submitting ? (
<ActivityIndicator size="small" color="#fff" />
) : (
{submitting ? (
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.dangerButtonText}></Text>
)}
</TouchableOpacity>

View File

@@ -16,13 +16,6 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度
const CONTENT_MAX_WIDTH = 720;
// 胡萝卜橙主题色
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
import { authService, resolveAuthApiError } from '../../services/authService';
import { showPrompt } from '../../services/promptService';
import { useAuthStore } from '../../stores';
@@ -263,9 +256,9 @@ export const AccountSecurityScreen: React.FC = () => {
onPress={handleSendCode}
disabled={sendingCode || countdown > 0}
>
{sendingCode ? (
<ActivityIndicator size="small" color="#fff" />
) : (
{sendingCode ? (
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
)}
</TouchableOpacity>
@@ -278,7 +271,7 @@ export const AccountSecurityScreen: React.FC = () => {
disabled={verifyingEmail}
>
{verifyingEmail ? (
<ActivityIndicator size="small" color="#fff" />
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.primaryButtonText}></Text>
)}
@@ -352,7 +345,7 @@ export const AccountSecurityScreen: React.FC = () => {
disabled={sendingChangePwdCode || changePwdCountdown > 0}
>
{sendingChangePwdCode ? (
<ActivityIndicator size="small" color="#fff" />
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.sendCodeButtonText}>
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
@@ -368,7 +361,7 @@ export const AccountSecurityScreen: React.FC = () => {
disabled={updatingPassword}
>
{updatingPassword ? (
<ActivityIndicator size="small" color="#fff" />
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.primaryButtonText}></Text>
)}
@@ -432,26 +425,26 @@ function createAccountSecurityStyles(colors: AppColors) {
borderRadius: borderRadius.sm,
},
statusVerified: {
backgroundColor: '#E8F5E9',
backgroundColor: colors.success.light + '30',
},
statusUnverified: {
backgroundColor: '#FFF3E0',
backgroundColor: colors.warning.light + '30',
},
statusText: {
fontSize: 13,
fontWeight: '600',
},
statusVerifiedText: {
color: '#2E7D32',
color: colors.success.dark,
},
statusUnverifiedText: {
color: '#E65100',
color: colors.warning.dark,
},
// 输入框样式 - 扁平化风格,与登录页一致
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F5F5F7',
backgroundColor: colors.background.default,
borderRadius: 14,
paddingHorizontal: spacing.lg,
height: 56,
@@ -485,13 +478,13 @@ function createAccountSecurityStyles(colors: AppColors) {
height: 56,
minWidth: 110,
borderRadius: 14,
backgroundColor: THEME_COLORS.primary,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.sm,
},
sendCodeButtonText: {
color: '#fff',
color: colors.text.inverse,
fontSize: fontSizes.sm,
fontWeight: '600',
},
@@ -499,14 +492,14 @@ function createAccountSecurityStyles(colors: AppColors) {
primaryButton: {
height: 56,
borderRadius: 14,
backgroundColor: THEME_COLORS.primary,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.xs,
marginHorizontal: spacing['2xl'],
},
primaryButtonText: {
color: '#fff',
color: colors.text.inverse,
fontSize: fontSizes.md,
fontWeight: '600',
},

View File

@@ -10,6 +10,8 @@ import {
StyleSheet,
Switch,
ScrollView,
Alert,
TouchableOpacity,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
@@ -22,6 +24,7 @@ import {
setVibrationPreference,
} from '../../services/notificationPreferences';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { backgroundSyncManager, BackgroundSyncMode } from '../../services/BackgroundSyncManager';
// 内容最大宽度
const CONTENT_MAX_WIDTH = 720;
@@ -41,6 +44,7 @@ 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>(BackgroundSyncMode.BATTERY_SAVER);
const { isWideScreen, isMobile } = useResponsive();
const insets = useSafeAreaInsets();
@@ -55,6 +59,7 @@ export const NotificationSettingsScreen: React.FC = () => {
setVibrationEnabledState(prefs.vibrationEnabled);
setPushEnabled(prefs.pushEnabled);
setSoundEnabled(prefs.soundEnabled);
setSyncMode(backgroundSyncManager.getMode());
} catch (error) {
console.error('加载通知设置失败:', error);
}
@@ -89,6 +94,51 @@ export const NotificationSettingsScreen: React.FC = () => {
}
};
const handleSyncModeChange = async (mode: BackgroundSyncMode) => {
if (mode === BackgroundSyncMode.REALTIME) {
Alert.alert(
'实时模式',
'实时模式会在通知栏显示常驻通知以保持应用后台运行,确保消息即时同步。\n\n' +
'建议在系统设置中将应用加入电池优化白名单以获得最佳效果。\n\n' +
'注意:此模式可能增加电池消耗。',
[
{ text: '取消', style: 'cancel' },
{
text: '开启',
onPress: async () => {
await backgroundSyncManager.setMode(mode);
setSyncMode(mode);
},
},
]
);
} else {
await backgroundSyncManager.setMode(mode);
setSyncMode(mode);
}
};
const syncModeOptions: { mode: BackgroundSyncMode; title: string; subtitle: string; icon: string }[] = [
{
mode: BackgroundSyncMode.BATTERY_SAVER,
title: '省电模式',
subtitle: '系统后台任务,每 15 分钟检查一次',
icon: 'leaf',
},
{
mode: BackgroundSyncMode.REALTIME,
title: '实时模式',
subtitle: '通知栏常驻保活,即时同步消息',
icon: 'lightning-bolt',
},
{
mode: BackgroundSyncMode.DISABLED,
title: '禁用',
subtitle: '仅在应用打开时接收消息',
icon: 'close-circle-outline',
},
];
const settings: NotificationSettingItem[] = [
{
key: 'push',
@@ -164,6 +214,62 @@ export const NotificationSettingsScreen: React.FC = () => {
</Text>
</View>
{/* 后台同步模式 */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text variant="caption" style={styles.sectionTitle}>
</Text>
</View>
{syncModeOptions.map((option) => (
<TouchableOpacity
key={option.mode}
style={[
styles.settingItem,
syncMode === option.mode && styles.settingItemActive,
]}
onPress={() => handleSyncModeChange(option.mode)}
activeOpacity={0.7}
>
<View style={styles.iconContainer}>
<MaterialCommunityIcons
name={option.icon as any}
size={22}
color={syncMode === option.mode ? colors.primary.main : colors.text.secondary}
/>
</View>
<View style={styles.settingContent}>
<Text
variant="body"
color={syncMode === option.mode ? colors.primary.main : colors.text.primary}
>
{option.title}
</Text>
<Text variant="caption" color={colors.text.secondary} style={styles.subtitle}>
{option.subtitle}
</Text>
</View>
{syncMode === option.mode && (
<MaterialCommunityIcons
name="check"
size={20}
color={colors.primary.main}
/>
)}
</TouchableOpacity>
))}
</View>
{/* 实时模式说明 */}
{syncMode === BackgroundSyncMode.REALTIME && (
<View style={styles.tipContainer}>
<MaterialCommunityIcons name="shield-check-outline" size={16} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.tipText}>
</Text>
</View>
)}
</>
);
@@ -210,6 +316,9 @@ function createNotificationSettingsStyles(colors: AppColors) {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
settingItemActive: {
backgroundColor: colors.primary.main + '0D',
},
iconContainer: {
width: 24,
height: 24,

View File

@@ -18,13 +18,6 @@ import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto';
const CONTENT_MAX_WIDTH = 720;
// 胡萝卜橙主题色
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
const VISIBILITY_OPTIONS: { value: VisibilityLevel; label: string; description: string }[] = [
{ value: 'everyone', label: '所有人可见', description: '任何人都可查看' },
{ value: 'following', label: '仅关注的人可见', description: '只有你关注的人可查看' },
@@ -74,7 +67,7 @@ function createPrivacySettingsStyles(colors: AppColors) {
},
// 隐私设置项 - 扁平化卡片风格
item: {
backgroundColor: '#F5F5F7',
backgroundColor: colors.background.default,
borderRadius: 14,
padding: spacing.lg,
marginHorizontal: spacing['2xl'],
@@ -111,15 +104,15 @@ function createPrivacySettingsStyles(colors: AppColors) {
backgroundColor: colors.background.paper,
},
optionButtonActive: {
backgroundColor: THEME_COLORS.primary,
borderColor: THEME_COLORS.primary,
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
optionText: {
fontSize: 13,
color: colors.text.primary,
},
optionTextActive: {
color: '#fff',
color: colors.text.inverse,
fontWeight: '500',
},
});

View File

@@ -26,12 +26,6 @@ import {
import { showPrompt } from '../../services/promptService';
import * as hrefs from '../../navigation/hrefs';
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
const STATUS_CONFIG = {
not_submitted: {
title: '未认证',
@@ -99,7 +93,7 @@ export const VerificationSettingsScreen: React.FC = () => {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
</SafeAreaView>
);
@@ -110,10 +104,11 @@ export const VerificationSettingsScreen: React.FC = () => {
const identityText = IDENTITY_TEXT[status?.identity || 'general'];
return (
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<SafeAreaView style={styles.container}>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 状态卡片 */}
<View style={styles.statusCard}>
<View style={[styles.statusIconContainer, { backgroundColor: statusConfig.color + '20' }]}>
@@ -128,7 +123,7 @@ export const VerificationSettingsScreen: React.FC = () => {
{status?.verification_status === 'approved' && (
<View style={styles.identityBadge}>
<MaterialCommunityIcons name="check-circle" size={16} color={THEME_COLORS.primary} />
<MaterialCommunityIcons name="check-circle" size={16} color={colors.primary.main} />
<Text style={styles.identityText}>{identityText}</Text>
</View>
)}
@@ -153,7 +148,7 @@ export const VerificationSettingsScreen: React.FC = () => {
<MaterialCommunityIcons
name="check-decagram"
size={24}
color={THEME_COLORS.primary}
color={colors.primary.main}
/>
</View>
<View style={styles.benefitContent}>
@@ -169,7 +164,7 @@ export const VerificationSettingsScreen: React.FC = () => {
<MaterialCommunityIcons
name="lock-open-outline"
size={24}
color={THEME_COLORS.primary}
color={colors.primary.main}
/>
</View>
<View style={styles.benefitContent}>
@@ -185,7 +180,7 @@ export const VerificationSettingsScreen: React.FC = () => {
<MaterialCommunityIcons
name="account-group-outline"
size={24}
color={THEME_COLORS.primary}
color={colors.primary.main}
/>
</View>
<View style={styles.benefitContent}>
@@ -208,7 +203,7 @@ export const VerificationSettingsScreen: React.FC = () => {
onPress={handleStartVerification}
activeOpacity={0.9}
>
<MaterialCommunityIcons name="shield-check-outline" size={20} color="#FFF" />
<MaterialCommunityIcons name="shield-check-outline" size={20} color={colors.primary.contrast} />
<Text style={styles.primaryButtonText}>
{status?.verification_status === 'rejected' ? '重新认证' : '开始认证'}
</Text>
@@ -221,7 +216,7 @@ export const VerificationSettingsScreen: React.FC = () => {
onPress={handleViewRecords}
activeOpacity={0.8}
>
<MaterialCommunityIcons name="file-document-outline" size={20} color={THEME_COLORS.primary} />
<MaterialCommunityIcons name="file-document-outline" size={20} color={colors.primary.main} />
<Text style={styles.secondaryButtonText}></Text>
</TouchableOpacity>
)}
@@ -262,6 +257,7 @@ export const VerificationSettingsScreen: React.FC = () => {
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
};
@@ -269,7 +265,7 @@ function createStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
backgroundColor: colors.background.paper,
},
loadingContainer: {
flex: 1,
@@ -307,7 +303,7 @@ function createStyles(colors: AppColors) {
alignItems: 'center',
paddingVertical: 32,
paddingHorizontal: 24,
backgroundColor: '#F9F9F9',
backgroundColor: colors.background.default,
borderRadius: 16,
marginBottom: 24,
},
@@ -334,7 +330,7 @@ function createStyles(colors: AppColors) {
identityBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFF5F0',
backgroundColor: colors.primary.main + '15',
borderRadius: 20,
paddingVertical: 8,
paddingHorizontal: 16,
@@ -343,25 +339,25 @@ function createStyles(colors: AppColors) {
identityText: {
fontSize: 14,
fontWeight: '600',
color: THEME_COLORS.primary,
color: colors.primary.main,
marginLeft: 6,
},
rejectReasonContainer: {
marginTop: 16,
padding: 12,
backgroundColor: '#FFF3F3',
backgroundColor: colors.error.light + '20',
borderRadius: 8,
width: '100%',
},
rejectReasonLabel: {
fontSize: 13,
fontWeight: '600',
color: '#F44336',
color: colors.error.main,
marginBottom: 4,
},
rejectReasonText: {
fontSize: 13,
color: '#D32F2F',
color: colors.error.dark,
lineHeight: 18,
},
benefitsSection: {
@@ -379,7 +375,7 @@ function createStyles(colors: AppColors) {
benefitItem: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: '#F9F9F9',
backgroundColor: colors.background.default,
borderRadius: 12,
padding: 16,
},
@@ -387,7 +383,7 @@ function createStyles(colors: AppColors) {
width: 48,
height: 48,
borderRadius: 12,
backgroundColor: '#FFF5F0',
backgroundColor: colors.primary.main + '15',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
@@ -415,12 +411,12 @@ function createStyles(colors: AppColors) {
justifyContent: 'center',
height: 56,
borderRadius: 14,
backgroundColor: THEME_COLORS.primary,
backgroundColor: colors.primary.main,
},
primaryButtonText: {
fontSize: 17,
fontWeight: '600',
color: '#FFF',
color: colors.text.inverse,
marginLeft: 8,
},
secondaryButton: {
@@ -429,18 +425,18 @@ function createStyles(colors: AppColors) {
justifyContent: 'center',
height: 56,
borderRadius: 14,
backgroundColor: '#FFF5F0',
backgroundColor: colors.primary.main + '15',
borderWidth: 1,
borderColor: THEME_COLORS.primary,
borderColor: colors.primary.main,
},
secondaryButtonText: {
fontSize: 17,
fontWeight: '600',
color: THEME_COLORS.primary,
color: colors.primary.main,
marginLeft: 8,
},
verifiedInfo: {
backgroundColor: '#F9F9F9',
backgroundColor: colors.background.default,
borderRadius: 12,
padding: 16,
},