feat(boot): 新增前台服务中转实现开机自启动

使用 BootService 前台服务绕过 Android 10+ 后台启动 Activity 限制,
通过 setFullScreenIntent 触发 InitActivity,兼容主从设备差异。
同时优化 MasterScaleActivity、SettingActivity 及相关布局。
This commit is contained in:
2026-04-09 18:20:50 +08:00
parent 9ae0d709f2
commit 904ccdbb06
9 changed files with 160 additions and 36 deletions
@@ -9,6 +9,7 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding
import com.shuwei.dish.match.databinding.ListItemScaleDataBinding
@@ -16,6 +17,7 @@ import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -138,7 +140,7 @@ class MasterScaleActivity : BaseActivity() {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).also { it.topMargin = 4.dpToPx() }
).also { it.topMargin = 4.dp }
}
row.addView(makeTextView("${scale.address}", 22, weight = 1f))
row.addView(makeTextView("${scale.weight} g", 26, bold = true, weight = 2f))
@@ -158,15 +160,11 @@ class MasterScaleActivity : BaseActivity() {
textSize = spSize.toFloat()
if (bold) setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(
if (bold) getColor(com.shuwei.dish.match.R.color.home_title)
else getColor(com.shuwei.dish.match.R.color.home_sub_title)
if (bold) getColor(R.color.home_title)
else getColor(R.color.home_sub_title)
)
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, weight)
}
}
/** dp 转 px 工具 */
private fun Int.dpToPx(): Int =
(this * resources.displayMetrics.density + 0.5f).toInt()
}
@@ -81,9 +81,9 @@ class SettingActivity : BaseActivity() {
updateButtonColors(isDeviceConfigSelected = false)
}
// 设备角色只读展示,角色由设备 ID 决定,不可手动切换
updateRoleButtonText()
binding.btnDeviceRole.isEnabled = false
// // 设备角色只读展示,角色由设备 ID 决定,不可手动切换
// updateRoleButtonText()
// binding.btnDeviceRole.isEnabled = false
// 主设备显示「秤数据监控」入口
if (GlobalData.deviceRole == DeviceRole.MASTER) {
@@ -97,11 +97,11 @@ class SettingActivity : BaseActivity() {
updateButtonColors(isDeviceConfigSelected = true)
}
/** 刷新角色按钮文字 */
private fun updateRoleButtonText() {
val roleLabel = if (GlobalData.deviceRole == DeviceRole.MASTER) "主设备" else "子设备"
binding.btnDeviceRole.text = "设备角色:$roleLabel"
}
// /** 刷新角色按钮文字 */
// private fun updateRoleButtonText() {
// val roleLabel = if (GlobalData.deviceRole == DeviceRole.MASTER) "主设备" else "子设备"
// binding.btnDeviceRole.text = "设备角色:$roleLabel"
// }
/**
* 更新底部菜单按钮的颜色状态
@@ -3,15 +3,25 @@ package com.shuwei.dish.match.utils
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.shuwei.dish.match.ui.InitActivity
class BootReceiver : BroadcastReceiver() {
companion object {
/** 兼容多种设备的开机广播 action 集合 */
private val BOOT_ACTIONS = setOf(
Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON",
"android.intent.action.LOCKED_BOOT_COMPLETED"
)
}
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
// 启动服务或Activity
val launchIntent = Intent(context, InitActivity::class.java)
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(launchIntent)
val action = intent.action ?: return
if (action in BOOT_ACTIONS) {
// Android 10+ 禁止在后台直接启动 Activity
// 改为启动前台服务,由 BootService 负责拉起 InitActivity
val serviceIntent = Intent(context, BootService::class.java)
context.startForegroundService(serviceIntent)
}
}
}
@@ -0,0 +1,95 @@
package com.shuwei.dish.match.utils
import android.app.ActivityManager
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.ui.InitActivity
/**
* 开机自启中转前台服务
* 用于绕过 Android 10+ 对后台直接启动 Activity 的限制
* 执行流程:BootReceiver → startForegroundService() → 本服务 → setFullScreenIntent → InitActivity
*/
class BootService : Service() {
companion object {
/** 通知渠道 ID */
private const val CHANNEL_ID = "boot_channel"
/** 前台通知 ID */
private const val NOTIFICATION_ID = 1001
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 若 App 进程已在前台运行(主设备系统签名场景),则不重复启动,避免闪退
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val isAppForeground = activityManager.runningAppProcesses?.any {
it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
&& it.processName == packageName
} ?: false
// App 未运行时才设置 fullScreenIntent,由系统自动启动 Activity
// 直接调用 startActivity() 在部分 Android 10 设备上会被 ActivityTaskManager 拦截
val pendingIntent = if (!isAppForeground) createLaunchPendingIntent() else null
// Android 14+ startForeground 需要传入 foregroundServiceType
val notification = buildNotification(pendingIntent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIFICATION_ID, notification)
}
stopSelf()
return START_NOT_STICKY
}
/** 创建启动 InitActivity 的 PendingIntent */
private fun createLaunchPendingIntent(): PendingIntent {
val launchIntent = Intent(this, InitActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
return PendingIntent.getActivity(
this, 0, launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
/**
* 构建前台服务通知
* fullScreenIntent 不为空时设置全屏意图,系统会在显示通知时自动启动 Activity
* 通知渠道必须为 IMPORTANCE_HIGH,否则 fullScreenIntent 不会触发
*/
private fun buildNotification(fullScreenIntent: PendingIntent?): Notification {
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
CHANNEL_ID,
"开机自启动",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "应用开机自动启动通知渠道"
setShowBadge(false)
}
manager.createNotificationChannel(channel)
return Notification.Builder(this, CHANNEL_ID)
.setContentTitle("正在启动应用")
.setSmallIcon(R.mipmap.ic_logo512)
.apply {
if (fullScreenIntent != null) {
setFullScreenIntent(fullScreenIntent, true)
}
}
.build()
}
}