Files
frontend/plugins/withForegroundService.js
lafay 437dab3b6e
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m51s
Frontend CI / ota-android (push) Successful in 11m16s
Frontend CI / build-android-apk (push) Failing after 12m54s
Rebrand the entire application from "胡萝卜社区" (Carrot BBS) to "威友" (WithYou), including:
- Update app name, package name (skin.carrot.bbs → cn.qczlit.withyou), and bundle identifier
- Update API endpoints (bbs.littlelan.cn → withyou.littlelan.cn)
- Update URL scheme (carrotbbs:// → withyou://)
- Rename database from carrot_bbs to with_you
- Update CI/CD pipeline image names and artifact naming
- Add Android signing configuration for release builds
- Update all UI text and comments throughout the codebase
- Refactor registerStore to use in-memory state instead of AsyncStorage persistence
- Improve WebRTC track handling and call stream management
- Add metro platform resolution for native platform support

BREAKING app package, database, and URLs are no longer valid
2026-04-22 16:54:51 +08:00

467 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { withAndroidManifest, withDangerousMod } = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');
const FOREGROUND_SERVICE_PERMISSION = 'android.permission.FOREGROUND_SERVICE';
const FOREGROUND_SERVICE_DATA_SYNC = 'android.permission.FOREGROUND_SERVICE_DATA_SYNC';
/**
* Expo Config Plugin为 Android 添加前台服务保活
*
* 功能:
* 1. 添加 FOREGROUND_SERVICE 相关权限
* 2. 在 AndroidManifest 中声明 Service
* 3. 生成 Kotlin 原生服务代码
* 4. 生成 React Native Bridge 模块
*
* 参考Element Android 的 GuardAndroidService
*/
const withForegroundService = (config, options = {}) => {
const {
notificationTitle = '威友',
notificationBody = '正在后台同步消息',
notificationIcon = 'ic_notification',
channelId = 'withyou_sync_foreground',
channelName = '消息同步',
} = options;
// 1. 添加权限和 Service 声明到 AndroidManifest
config = withAndroidManifest(config, (config) => {
const manifest = config.modResults;
// 添加权限声明
const permissions = [
FOREGROUND_SERVICE_PERMISSION,
FOREGROUND_SERVICE_DATA_SYNC,
];
permissions.forEach((permission) => {
const existingPermission = manifest.manifest['uses-permission']?.find(
(p) => p.$['android:name'] === permission
);
if (!existingPermission) {
if (!manifest.manifest['uses-permission']) {
manifest.manifest['uses-permission'] = [];
}
manifest.manifest['uses-permission'].push({
$: { 'android:name': permission },
});
}
});
// 添加 Service 声明
if (!manifest.manifest.application[0].service) {
manifest.manifest.application[0].service = [];
}
const existingService = manifest.manifest.application[0].service.find(
(s) => s.$['android:name'] === '.SyncForegroundService'
);
if (!existingService) {
manifest.manifest.application[0].service.push({
$: {
'android:name': '.SyncForegroundService',
'android:enabled': 'true',
'android:exported': 'false',
'android:foregroundServiceType': 'dataSync',
},
});
}
return config;
});
// 2. 生成 Kotlin 原生代码
config = withDangerousMod(config, [
'android',
async (config) => {
const projectRoot = config.modRequest.projectRoot;
const packageName = config.android.package;
const kotlinPath = path.join(
projectRoot,
'android/app/src/main/java',
packageName.replace(/\./g, '/')
);
// 确保目录存在
if (!fs.existsSync(kotlinPath)) {
fs.mkdirSync(kotlinPath, { recursive: true });
}
// 生成 SyncForegroundService.kt
const serviceCode = generateServiceCode(packageName, {
notificationTitle,
notificationBody,
notificationIcon,
channelId,
channelName,
});
fs.writeFileSync(
path.join(kotlinPath, 'SyncForegroundService.kt'),
serviceCode
);
// 生成 ForegroundServiceModule.kt (React Native Bridge)
const moduleCode = generateModuleCode(packageName);
fs.writeFileSync(
path.join(kotlinPath, 'ForegroundServiceModule.kt'),
moduleCode
);
// 生成 ForegroundServicePackage.kt
const packageCode = generatePackageCode(packageName);
fs.writeFileSync(
path.join(kotlinPath, 'ForegroundServicePackage.kt'),
packageCode
);
// 修改 MainApplication.kt 注册模块
const mainAppPath = path.join(kotlinPath, 'MainApplication.kt');
if (fs.existsSync(mainAppPath)) {
let content = fs.readFileSync(mainAppPath, 'utf-8');
// 添加 import
if (!content.includes('ForegroundServicePackage')) {
content = content.replace(
/package .*\n/,
`$&\nimport ${packageName}.ForegroundServicePackage\n`
);
}
// 在 packages 列表中添加
if (!content.includes('ForegroundServicePackage()')) {
// 查找 PackageList(this).packages.apply 块
content = content.replace(
/(PackageList\(this\)\.packages\.apply\s*\{)/,
`$1\n // 前台服务模块\n add(ForegroundServicePackage())`
);
}
fs.writeFileSync(mainAppPath, content);
}
// 创建通知图标目录和文件
const drawablePath = path.join(projectRoot, 'android/app/src/main/res/drawable');
if (!fs.existsSync(drawablePath)) {
fs.mkdirSync(drawablePath, { recursive: true });
}
const iconPath = path.join(drawablePath, 'ic_notification.xml');
if (!fs.existsSync(iconPath)) {
fs.writeFileSync(iconPath, generateNotificationIcon());
}
return config;
},
]);
return config;
};
/**
* 生成前台服务 Kotlin 代码
*/
function generateServiceCode(packageName, options) {
const { notificationTitle, notificationBody, notificationIcon, channelId, channelName } = options;
return `package ${packageName}
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
/**
* 前台同步服务
*
* 在通知栏显示常驻通知,防止应用进程被系统杀死
* 类似 Element Android 的 GuardAndroidService 和 V2Ray 的保活机制
*/
class SyncForegroundService : Service() {
companion object {
const val NOTIFICATION_ID = 1001
const val CHANNEL_ID = "${channelId}"
const val CHANNEL_NAME = "${channelName}"
const val ACTION_START = "${packageName}.foreground.START"
const val ACTION_STOP = "${packageName}.foreground.STOP"
const val ACTION_UPDATE = "${packageName}.foreground.UPDATE"
const val EXTRA_TITLE = "title"
const val EXTRA_BODY = "body"
/**
* 启动前台服务
*/
fun start(context: Context, title: String = "${notificationTitle}", body: String = "${notificationBody}") {
val intent = Intent(context, SyncForegroundService::class.java).apply {
action = ACTION_START
putExtra(EXTRA_TITLE, title)
putExtra(EXTRA_BODY, body)
}
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: Exception) {
android.util.Log.e("SyncForegroundService", "启动前台服务失败", e)
}
}
/**
* 停止前台服务
*/
fun stop(context: Context) {
val intent = Intent(context, SyncForegroundService::class.java).apply {
action = ACTION_STOP
}
context.startService(intent)
}
/**
* 更新通知内容
*/
fun update(context: Context, title: String, body: String) {
val intent = Intent(context, SyncForegroundService::class.java).apply {
action = ACTION_UPDATE
putExtra(EXTRA_TITLE, title)
putExtra(EXTRA_BODY, body)
}
context.startService(intent)
}
}
private var notificationTitle: String = "${notificationTitle}"
private var notificationBody: String = "${notificationBody}"
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
notificationTitle = intent.getStringExtra(EXTRA_TITLE) ?: "${notificationTitle}"
notificationBody = intent.getStringExtra(EXTRA_BODY) ?: "${notificationBody}"
startForegroundCompat()
}
ACTION_STOP -> {
stopForegroundCompat()
stopSelf()
}
ACTION_UPDATE -> {
notificationTitle = intent.getStringExtra(EXTRA_TITLE) ?: notificationTitle
notificationBody = intent.getStringExtra(EXTRA_BODY) ?: notificationBody
updateNotification()
}
}
// START_STICKY: 服务被杀后自动重启
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
/**
* 启动前台服务(兼容 Android Q+
*/
private fun startForegroundCompat() {
val notification = buildNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
}
/**
* 停止前台服务(兼容 Android N+
*/
private fun stopForegroundCompat() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
}
/**
* 创建通知渠道
*/
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_MIN // 最低重要性,不打扰用户
).apply {
description = "后台消息同步服务"
setSound(null, null)
setShowBadge(false)
enableLights(false)
enableVibration(false)
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
/**
* 构建通知
*/
private fun buildNotification(): Notification {
// 点击通知打开应用
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
val pendingIntent = PendingIntent.getActivity(
this,
0,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(notificationTitle)
.setContentText(notificationBody)
.setSmallIcon(R.drawable.${notificationIcon})
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true) // 不可滑动清除
.setShowWhen(false)
.setContentIntent(pendingIntent)
.build()
}
/**
* 更新通知内容
*/
private fun updateNotification() {
val manager = getSystemService(NotificationManager::class.java)
manager.notify(NOTIFICATION_ID, buildNotification())
}
}
`;
}
/**
* 生成 React Native Bridge 模块代码
*/
function generateModuleCode(packageName) {
return `package ${packageName}
import android.content.Context
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
/**
* React Native Bridge: 前台服务控制模块
*/
class ForegroundServiceModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "ForegroundService"
/**
* 启动前台服务
*/
@ReactMethod
fun start(title: String, body: String, promise: Promise) {
try {
SyncForegroundService.start(reactApplicationContext, title, body)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("FOREGROUND_SERVICE_ERROR", e.message)
}
}
/**
* 停止前台服务
*/
@ReactMethod
fun stop(promise: Promise) {
try {
SyncForegroundService.stop(reactApplicationContext)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("FOREGROUND_SERVICE_ERROR", e.message)
}
}
/**
* 更新通知内容
*/
@ReactMethod
fun update(title: String, body: String, promise: Promise) {
try {
SyncForegroundService.update(reactApplicationContext, title, body)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("FOREGROUND_SERVICE_ERROR", e.message)
}
}
}
`;
}
/**
* 生成 React Native Package 代码
*/
function generatePackageCode(packageName) {
return `package ${packageName}
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class ForegroundServicePackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(ForegroundServiceModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
`;
}
/**
* 生成通知图标 XML
*/
function generateNotificationIcon() {
return `<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 背景圆 -->
<path
android:fillColor="#4CAF50"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
<!-- 对勾符号 -->
<path
android:fillColor="#FFFFFF"
android:pathData="M10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>
`;
}
module.exports = withForegroundService;