feat(boot): 新增前台服务中转实现开机自启动
使用 BootService 前台服务绕过 Android 10+ 后台启动 Activity 限制, 通过 setFullScreenIntent 触发 InitActivity,兼容主从设备差异。 同时优化 MasterScaleActivity、SettingActivity 及相关布局。
This commit is contained in:
@@ -30,7 +30,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置输出APK文件名格式
|
// 设置输出APK文件名格式
|
||||||
setProperty("archivesBaseName", "菜品配比终端v${versionName}")
|
setProperty("archivesBaseName", "pbc_v${versionName}")
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("debug_507") {
|
create("debug_507") {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
@@ -105,12 +107,24 @@
|
|||||||
<receiver
|
<receiver
|
||||||
android:name=".utils.BootReceiver"
|
android:name=".utils.BootReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:directBootAware="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<!-- 开机自启中转前台服务,exported=false 仅允许本应用内部启动 -->
|
||||||
|
<service
|
||||||
|
android:name=".utils.BootService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:directBootAware="true" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -9,6 +9,7 @@ import android.widget.TextView
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.shuwei.dish.match.R
|
||||||
import com.shuwei.dish.match.base.BaseActivity
|
import com.shuwei.dish.match.base.BaseActivity
|
||||||
import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding
|
import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding
|
||||||
import com.shuwei.dish.match.databinding.ListItemScaleDataBinding
|
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.scale.ScaleServiceManager
|
||||||
import com.shuwei.dish.match.utils.NetworkUtil
|
import com.shuwei.dish.match.utils.NetworkUtil
|
||||||
import com.shuwei.dish.match.utils.WeightUtil
|
import com.shuwei.dish.match.utils.WeightUtil
|
||||||
|
import com.shuwei.dish.match.utils.ext.dp
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -138,7 +140,7 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
layoutParams = LinearLayout.LayoutParams(
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
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.address}", 22, weight = 1f))
|
||||||
row.addView(makeTextView("${scale.weight} g", 26, bold = true, weight = 2f))
|
row.addView(makeTextView("${scale.weight} g", 26, bold = true, weight = 2f))
|
||||||
@@ -158,15 +160,11 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
textSize = spSize.toFloat()
|
textSize = spSize.toFloat()
|
||||||
if (bold) setTypeface(null, android.graphics.Typeface.BOLD)
|
if (bold) setTypeface(null, android.graphics.Typeface.BOLD)
|
||||||
setTextColor(
|
setTextColor(
|
||||||
if (bold) getColor(com.shuwei.dish.match.R.color.home_title)
|
if (bold) getColor(R.color.home_title)
|
||||||
else getColor(com.shuwei.dish.match.R.color.home_sub_title)
|
else getColor(R.color.home_sub_title)
|
||||||
)
|
)
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, weight)
|
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)
|
updateButtonColors(isDeviceConfigSelected = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备角色只读展示,角色由设备 ID 决定,不可手动切换
|
// // 设备角色只读展示,角色由设备 ID 决定,不可手动切换
|
||||||
updateRoleButtonText()
|
// updateRoleButtonText()
|
||||||
binding.btnDeviceRole.isEnabled = false
|
// binding.btnDeviceRole.isEnabled = false
|
||||||
|
|
||||||
// 主设备显示「秤数据监控」入口
|
// 主设备显示「秤数据监控」入口
|
||||||
if (GlobalData.deviceRole == DeviceRole.MASTER) {
|
if (GlobalData.deviceRole == DeviceRole.MASTER) {
|
||||||
@@ -97,11 +97,11 @@ class SettingActivity : BaseActivity() {
|
|||||||
updateButtonColors(isDeviceConfigSelected = true)
|
updateButtonColors(isDeviceConfigSelected = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 刷新角色按钮文字 */
|
// /** 刷新角色按钮文字 */
|
||||||
private fun updateRoleButtonText() {
|
// private fun updateRoleButtonText() {
|
||||||
val roleLabel = if (GlobalData.deviceRole == DeviceRole.MASTER) "主设备" else "子设备"
|
// val roleLabel = if (GlobalData.deviceRole == DeviceRole.MASTER) "主设备" else "子设备"
|
||||||
binding.btnDeviceRole.text = "设备角色:$roleLabel"
|
// binding.btnDeviceRole.text = "设备角色:$roleLabel"
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新底部菜单按钮的颜色状态
|
* 更新底部菜单按钮的颜色状态
|
||||||
|
|||||||
@@ -3,15 +3,25 @@ package com.shuwei.dish.match.utils
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.shuwei.dish.match.ui.InitActivity
|
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
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) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
|
val action = intent.action ?: return
|
||||||
// 启动服务或Activity
|
if (action in BOOT_ACTIONS) {
|
||||||
val launchIntent = Intent(context, InitActivity::class.java)
|
// Android 10+ 禁止在后台直接启动 Activity
|
||||||
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
// 改为启动前台服务,由 BootService 负责拉起 InitActivity
|
||||||
context.startActivity(launchIntent)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
android:text="全设备秤数据监控"
|
android:text="全设备秤数据监控"
|
||||||
android:textColor="@color/home_title"
|
android:textColor="@color/home_title"
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:overScrollMode="never"
|
android:overScrollMode="never"
|
||||||
android:padding="16dp" />
|
android:padding="16dp"
|
||||||
|
tools:listitem="@layout/list_item_scale_data"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -37,14 +37,14 @@
|
|||||||
android:textSize="26sp"
|
android:textSize="26sp"
|
||||||
android:background="@android:color/transparent" />
|
android:background="@android:color/transparent" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
<!-- <androidx.appcompat.widget.AppCompatButton-->
|
||||||
android:id="@+id/btnDeviceRole"
|
<!-- android:id="@+id/btnDeviceRole"-->
|
||||||
android:layout_width="0dp"
|
<!-- android:layout_width="0dp"-->
|
||||||
android:layout_height="70dp"
|
<!-- android:layout_height="70dp"-->
|
||||||
android:layout_weight="1"
|
<!-- android:layout_weight="1"-->
|
||||||
android:text="设备角色:子设备"
|
<!-- android:text="设备角色:子设备"-->
|
||||||
android:textSize="22sp"
|
<!-- android:textSize="22sp"-->
|
||||||
android:background="@android:color/transparent" />
|
<!-- android:background="@android:color/transparent" />-->
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
android:id="@+id/btnScaleMonitor"
|
android:id="@+id/btnScaleMonitor"
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
android:paddingTop="12dp"
|
android:paddingTop="10dp">
|
||||||
android:paddingBottom="12dp">
|
|
||||||
|
|
||||||
<!-- 设备 IP -->
|
<!-- 设备 IP -->
|
||||||
<TextView
|
<TextView
|
||||||
@@ -17,6 +16,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
android:includeFontPadding="false"
|
||||||
android:textColor="@color/home_title"
|
android:textColor="@color/home_title"
|
||||||
tools:text="IP:192.168.1.100" />
|
tools:text="IP:192.168.1.100" />
|
||||||
|
|
||||||
@@ -37,4 +37,10 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_marginTop="5dp" />
|
android:layout_marginTop="5dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.divider.MaterialDivider
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="@color/gray_eb"
|
||||||
|
android:layout_marginTop="10dp"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user