- 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
467 lines
14 KiB
JavaScript
467 lines
14 KiB
JavaScript
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; |