本章深入 Android 系统特性:运行时权限、FCM 推送、CameraX 相机、位置服务、生物识别、WorkManager 后台任务,以及应用内更新等核心平台能力的完整实现。
| 节 |
主题 |
| 9.1 |
运行时权限 |
| 9.2 |
推送通知(FCM) |
| 9.3 |
相机与媒体(CameraX) |
| 9.4 |
位置服务 |
| 9.5 |
生物识别(BiometricPrompt) |
| 9.6 |
后台任务(WorkManager) |
| 9.7 |
应用内更新 |
| 9.8 |
蓝牙基础 |
9.1 运行时权限
Compose 中的权限请求
kotlin
复制代码
// build.gradle.kts
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPermissionScreen(onPermissionGranted: () -> Unit) {
val cameraPermissionState = rememberPermissionState(
permission = Manifest.permission.CAMERA
)
LaunchedEffect(cameraPermissionState.status) {
if (cameraPermissionState.status.isGranted) {
onPermissionGranted()
}
}
when {
cameraPermissionState.status.isGranted -> {
// 权限已授予
CameraContent()
}
cameraPermissionState.status.shouldShowRationale -> {
// 需要向用户解释为何需要权限(用户曾拒绝过)
PermissionRationaleDialog(
title = "需要相机权限",
description = "拍照功能需要访问您的相机,请授权以继续。",
onConfirm = { cameraPermissionState.launchPermissionRequest() },
onDismiss = { }
)
}
else -> {
// 首次请求或永久拒绝后需要打开设置
PermissionDeniedContent(
onRequestPermission = { cameraPermissionState.launchPermissionRequest() }
)
}
}
}
// 多权限请求
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun LocationPermissionHandler(
onPermissionsGranted: () -> Unit
) {
val locationPermissions = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
) { permissionsMap ->
if (permissionsMap.values.any { it }) {
onPermissionsGranted()
}
}
LaunchedEffect(Unit) {
if (!locationPermissions.allPermissionsGranted) {
locationPermissions.launchMultiplePermissionRequest()
}
}
}
@Composable
private fun PermissionRationaleDialog(
title: String, description: String,
onConfirm: () -> Unit, onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = { Text(description) },
confirmButton = { TextButton(onClick = onConfirm) { Text("授权") } },
dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
)
}
@Composable
private fun PermissionDeniedContent(onRequestPermission: () -> Unit) {
val context = LocalContext.current
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(72.dp))
Spacer(Modifier.height(16.dp))
Text("相机权限未授予", style = MaterialTheme.typography.headlineSmall)
Spacer(Modifier.height(8.dp))
Text("请在系统设置中手动开启相机权限", textAlign = TextAlign.Center)
Spacer(Modifier.height(24.dp))
Button(onClick = {
// 跳转到应用设置
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
context.startActivity(this)
}
}) { Text("前往设置") }
Spacer(Modifier.height(8.dp))
OutlinedButton(onClick = onRequestPermission) { Text("重新请求权限") }
}
}
@Composable private fun CameraContent() { Text("相机界面") }
传统方式(ActivityResultLauncher)
kotlin
复制代码
class PermissionActivity : AppCompatActivity() {
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.values.all { it }
if (allGranted) {
startCamera()
} else {
val permanentlyDenied = permissions.entries.any { (perm, granted) ->
!granted && !shouldShowRequestPermissionRationale(perm)
}
if (permanentlyDenied) {
showGoToSettingsDialog()
} else {
showRationaleDialog()
}
}
}
private fun checkAndRequestPermissions() {
val required = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
val ungrantedPermissions = required.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (ungrantedPermissions.isEmpty()) {
startCamera()
} else {
requestPermissionLauncher.launch(ungrantedPermissions.toTypedArray())
}
}
private fun startCamera() { /* ... */ }
private fun showGoToSettingsDialog() { /* ... */ }
private fun showRationaleDialog() { /* ... */ }
}
9.2 推送通知(FCM)
FCM 集成
kotlin
复制代码
// FCM Service
@AndroidEntryPoint
class MyFirebaseMessagingService : FirebaseMessagingService() {
@Inject lateinit var notificationManager: AppNotificationManager
override fun onNewToken(token: String) {
super.onNewToken(token)
// 将新 Token 上传到服务器
sendRegistrationToServer(token)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
// 处理数据消息(App 运行或后台都能收到)
val data = remoteMessage.data
if (data.isNotEmpty()) {
handleDataMessage(data)
}
// 处理通知消息(App 前台时收到,后台由系统处理)
remoteMessage.notification?.let { notification ->
notificationManager.showNotification(
title = notification.title ?: "",
body = notification.body ?: "",
channelId = CHANNEL_GENERAL
)
}
}
private fun handleDataMessage(data: Map<String, String>) {
val type = data["type"]
when (type) {
"order_update" -> {
val orderId = data["order_id"]
val status = data["status"]
notificationManager.showOrderNotification(orderId, status)
}
"promotion" -> {
// 处理促销推送
}
"chat" -> {
// 处理聊天消息
}
}
}
private fun sendRegistrationToServer(token: String) {
CoroutineScope(Dispatchers.IO).launch {
// 上传 Token 到服务器
}
}
companion object {
const val CHANNEL_GENERAL = "general"
const val CHANNEL_ORDERS = "orders"
const val CHANNEL_CHAT = "chat"
}
}
// 通知管理器
class AppNotificationManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private val notificationManager =
context.getSystemService(NotificationManager::class.java)
init {
createNotificationChannels()
}
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channels = listOf(
NotificationChannel(
MyFirebaseMessagingService.CHANNEL_GENERAL,
"通用通知",
NotificationManager.IMPORTANCE_DEFAULT
).apply { description = "通用推送消息" },
NotificationChannel(
MyFirebaseMessagingService.CHANNEL_ORDERS,
"订单通知",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "订单状态更新"
enableVibration(true)
enableLights(true)
lightColor = Color.BLUE
},
NotificationChannel(
MyFirebaseMessagingService.CHANNEL_CHAT,
"消息通知",
NotificationManager.IMPORTANCE_HIGH
).apply { description = "聊天消息" }
)
notificationManager.createNotificationChannels(channels)
}
}
fun showNotification(title: String, body: String, channelId: String) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
}
// 富文本通知(带图片)
fun showBigPictureNotification(title: String, body: String, imageUrl: String) {
val bitmap = BitmapFactory.decodeStream(URL(imageUrl).openStream())
val notification = NotificationCompat.Builder(context, MyFirebaseMessagingService.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(bitmap))
.build()
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
}
// MessageStyle(聊天气泡)
fun showChatNotification(sender: String, messages: List<String>) {
val person = Person.Builder().setName(sender).build()
val style = NotificationCompat.MessagingStyle(person).apply {
messages.forEach { msg ->
addMessage(msg, System.currentTimeMillis(), person)
}
}
val notification = NotificationCompat.Builder(context, MyFirebaseMessagingService.CHANNEL_CHAT)
.setSmallIcon(R.drawable.ic_notification)
.setStyle(style)
.build()
notificationManager.notify(sender.hashCode(), notification)
}
fun showOrderNotification(orderId: String?, status: String?) {
showNotification("订单更新", "订单 #$orderId:$status", MyFirebaseMessagingService.CHANNEL_ORDERS)
}
}
9.3 相机与媒体(CameraX)
kotlin
复制代码
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraScreen() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA)
if (!cameraPermission.status.isGranted) {
LaunchedEffect(Unit) { cameraPermission.launchPermissionRequest() }
return
}
var imageCapture: ImageCapture? by remember { mutableStateOf(null) }
var flashEnabled by remember { mutableStateOf(false) }
var isCapturing by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxSize()) {
// 相机预览
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx)
val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.surfaceProvider = previewView.surfaceProvider
}
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.setFlashMode(
if (flashEnabled) ImageCapture.FLASH_MODE_ON
else ImageCapture.FLASH_MODE_OFF
)
.build()
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture
)
} catch (e: Exception) {
Log.e("Camera", "bindToLifecycle failed", e)
}
}, ContextCompat.getMainExecutor(ctx))
previewView
},
modifier = Modifier.fillMaxSize()
)
// 控制按钮
Row(
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 48.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 闪光灯
IconButton(onClick = { flashEnabled = !flashEnabled }) {
Icon(
imageVector = if (flashEnabled) Icons.Default.FlashOn else Icons.Default.FlashOff,
contentDescription = "Flash",
tint = Color.White
)
}
// 拍照按钮
FloatingActionButton(
onClick = {
if (!isCapturing) {
isCapturing = true
capturePhoto(context, imageCapture) { isCapturing = false }
}
},
containerColor = Color.White,
modifier = Modifier.size(72.dp)
) {
if (isCapturing) {
CircularProgressIndicator(modifier = Modifier.size(32.dp))
} else {
Icon(Icons.Default.Camera, contentDescription = "拍照", tint = Color.Black)
}
}
}
}
}
private fun capturePhoto(
context: Context,
imageCapture: ImageCapture?,
onComplete: () -> Unit
) {
val imageCapture = imageCapture ?: run { onComplete(); return }
val outputOptions = ImageCapture.OutputFileOptions.Builder(
context.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "photo_${System.currentTimeMillis()}")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
}
}
).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
Log.d("Camera", "Photo saved: ${output.savedUri}")
onComplete()
}
override fun onError(exc: ImageCaptureException) {
Log.e("Camera", "Photo capture failed: ${exc.message}", exc)
onComplete()
}
}
)
}
// 图片选择器(Android 13+ Photo Picker)
class PhotoPicker(activity: ComponentActivity) {
private val pickSingleMedia = activity.registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
uri?.let { handleSelectedPhoto(it) }
}
private val pickMultipleMedia = activity.registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(maxItems = 5)
) { uris ->
uris.forEach { handleSelectedPhoto(it) }
}
fun pickSingle() {
pickSingleMedia.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
fun pickMultiple() {
pickMultipleMedia.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)
)
}
private fun handleSelectedPhoto(uri: Uri) { /* 处理选中的图片 */ }
}
9.4 位置服务
kotlin
复制代码
// FusedLocationProviderClient(最准确、最省电)
@Singleton
class LocationService @Inject constructor(
@ApplicationContext private val context: Context
) {
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
// 获取最后已知位置(快速,不一定最新)
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
suspend fun getLastLocation(): Location? = suspendCancellableCoroutine { cont ->
fusedLocationClient.lastLocation
.addOnSuccessListener { location -> cont.resume(location) }
.addOnFailureListener { e -> cont.resumeWithException(e) }
}
// 持续位置更新(CallbackFlow 包装)
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
fun locationUpdates(intervalMs: Long = 10_000L): Flow<Location> = callbackFlow {
val request = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
intervalMs
).setMinUpdateIntervalMillis(5_000L).build()
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.locations.forEach { location ->
trySend(location)
}
}
}
fusedLocationClient.requestLocationUpdates(
request,
callback,
Looper.getMainLooper()
)
awaitClose {
fusedLocationClient.removeLocationUpdates(callback)
}
}
// 地理编码(坐标 → 地址)
suspend fun reverseGeocode(latitude: Double, longitude: Double): String? {
return withContext(Dispatchers.IO) {
val geocoder = Geocoder(context, Locale.getDefault())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
suspendCancellableCoroutine { cont ->
geocoder.getFromLocation(latitude, longitude, 1) { addresses ->
cont.resume(addresses.firstOrNull()?.getAddressLine(0))
}
}
} else {
@Suppress("DEPRECATION")
geocoder.getFromLocation(latitude, longitude, 1)
?.firstOrNull()?.getAddressLine(0)
}
}
}
}
// Compose 中使用位置
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun LocationScreen(locationService: LocationService) {
var currentLocation by remember { mutableStateOf<Location?>(null) }
var address by remember { mutableStateOf("") }
val locationPermissions = rememberMultiplePermissionsState(
listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
LaunchedEffect(locationPermissions.allPermissionsGranted) {
if (locationPermissions.allPermissionsGranted) {
locationService.locationUpdates(intervalMs = 5_000L).collect { location ->
currentLocation = location
address = locationService.reverseGeocode(location.latitude, location.longitude) ?: ""
}
}
}
Column(modifier = Modifier.padding(16.dp)) {
currentLocation?.let { location ->
Text("纬度: %.6f".format(location.latitude))
Text("经度: %.6f".format(location.longitude))
Text("精度: ${location.accuracy}m")
if (address.isNotBlank()) Text("地址: $address")
} ?: Text("等待位置信息...")
if (!locationPermissions.allPermissionsGranted) {
Button(onClick = { locationPermissions.launchMultiplePermissionRequest() }) {
Text("请求位置权限")
}
}
}
}
9.5 生物识别(BiometricPrompt)
kotlin
复制代码
class BiometricAuthManager @Inject constructor(
@ApplicationContext private val context: Context
) {
fun canAuthenticate(): BiometricAvailability {
val biometricManager = BiometricManager.from(context)
return when (biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)) {
BiometricManager.BIOMETRIC_SUCCESS -> BiometricAvailability.Available
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricAvailability.NoHardware
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> BiometricAvailability.HardwareUnavailable
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricAvailability.NotEnrolled
else -> BiometricAvailability.Unknown
}
}
fun authenticate(
activity: FragmentActivity,
title: String = "身份验证",
subtitle: String = "使用指纹或面容验证",
onSuccess: () -> Unit,
onError: (String) -> Unit
) {
if (canAuthenticate() != BiometricAvailability.Available) {
onError("设备不支持生物识别")
return
}
val executor = ContextCompat.getMainExecutor(activity)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
.build()
val biometricPrompt = BiometricPrompt(activity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onSuccess()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onError(errString.toString())
}
override fun onAuthenticationFailed() {
// 单次认证失败(不是最终失败)
}
}
)
biometricPrompt.authenticate(promptInfo)
}
// 结合 Android Keystore:加密存储
fun authenticateWithKeystore(
activity: FragmentActivity,
cryptoObject: BiometricPrompt.CryptoObject,
onSuccess: (BiometricPrompt.AuthenticationResult) -> Unit,
onError: (String) -> Unit
) {
val executor = ContextCompat.getMainExecutor(activity)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("请验证身份")
.setNegativeButtonText("取消")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()
BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onSuccess(result)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onError(errString.toString())
}
}).authenticate(promptInfo, cryptoObject)
}
}
enum class BiometricAvailability {
Available, NoHardware, HardwareUnavailable, NotEnrolled, Unknown
}
// Compose 集成
@Composable
fun BiometricButton(activity: FragmentActivity, biometricManager: BiometricAuthManager) {
var authResult by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
Text(authResult)
Spacer(Modifier.height(16.dp))
Button(onClick = {
biometricManager.authenticate(
activity = activity,
title = "验证身份",
subtitle = "请使用指纹或面容 ID 解锁",
onSuccess = { authResult = "✅ 验证成功!" },
onError = { error -> authResult = "❌ 验证失败:$error" }
)
}) { Text("生物识别验证") }
}
}
9.6 后台任务(WorkManager)
kotlin
复制代码
// 带进度报告的 Worker
class ImageUploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val imageUri = inputData.getString("image_uri")
?: return Result.failure(workDataOf("error" to "No image URI"))
return try {
setForeground(createForegroundInfo())
// 分阶段报告进度
setProgress(workDataOf("progress" to 0))
val compressedFile = compressImage(Uri.parse(imageUri))
setProgress(workDataOf("progress" to 30))
val uploadedUrl = uploadToServer(compressedFile)
setProgress(workDataOf("progress" to 100))
Result.success(workDataOf("url" to uploadedUrl))
} catch (e: CancellationException) {
throw e // 必须重新抛出
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry()
else Result.failure(workDataOf("error" to e.message))
}
}
private fun createForegroundInfo(): ForegroundInfo {
val notification = NotificationCompat.Builder(applicationContext, "upload")
.setSmallIcon(R.drawable.ic_upload)
.setContentTitle("正在上传图片")
.setProgress(100, 0, true)
.setOngoing(true)
.build()
return ForegroundInfo(1002, notification)
}
private suspend fun compressImage(uri: Uri): File = withContext(Dispatchers.IO) { File("") }
private suspend fun uploadToServer(file: File): String = withContext(Dispatchers.IO) { "" }
}
// 任务链(Chain)
class WorkScheduler @Inject constructor(
private val workManager: WorkManager
) {
fun scheduleImageProcessingChain(imageUris: List<Uri>): LiveData<WorkInfo> {
val uploadRequests = imageUris.map { uri ->
OneTimeWorkRequestBuilder<ImageUploadWorker>()
.setInputData(workDataOf("image_uri" to uri.toString()))
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
}
val notifyRequest = OneTimeWorkRequestBuilder<NotifyWorker>()
.setInputMerger(ArrayCreatingInputMerger::class) // 合并多个输入
.build()
return workManager
.beginWith(uploadRequests)
.then(notifyRequest)
.enqueue()
.workInfosLiveData
.map { it.lastOrNull() }
}
// 周期任务(数据同步)
fun scheduleDailySync() {
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.DAYS)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // 仅 WiFi
.setRequiresCharging(true) // 仅充电时
.build()
)
.setInitialDelay(1, TimeUnit.HOURS)
.build()
workManager.enqueueUniquePeriodicWork(
"daily_sync",
ExistingPeriodicWorkPolicy.KEEP, // 已有则保留
syncRequest
)
}
}
class NotifyWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result { return Result.success() }
}
9.7 应用内更新
kotlin
复制代码
class InAppUpdateManager @Inject constructor(
private val activity: AppCompatActivity
) {
private val appUpdateManager = AppUpdateManagerFactory.create(activity)
fun checkForUpdate() {
appUpdateManager.appUpdateInfo.addOnSuccessListener { info ->
when {
info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) -> {
// 灵活更新(后台下载,用户可继续使用)
startFlexibleUpdate(info)
}
info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) -> {
// 强制更新(必须完成才能使用)
startImmediateUpdate(info)
}
}
}
}
private fun startFlexibleUpdate(info: AppUpdateInfo) {
appUpdateManager.startUpdateFlowForResult(
info,
activity,
AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(),
UPDATE_REQUEST_CODE
)
// 监听下载进度
appUpdateManager.registerListener { state ->
when (state.installStatus()) {
InstallStatus.DOWNLOADED -> {
// 下载完成,提示用户重启
showRestartSnackbar()
}
InstallStatus.DOWNLOADING -> {
val progress = state.bytesDownloaded().toFloat() / state.totalBytesToDownload()
updateProgressUI(progress)
}
InstallStatus.FAILED -> {
showUpdateFailedDialog()
}
}
}
}
private fun startImmediateUpdate(info: AppUpdateInfo) {
appUpdateManager.startUpdateFlowForResult(
info,
activity,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
UPDATE_REQUEST_CODE
)
}
private fun showRestartSnackbar() {
Snackbar.make(
activity.findViewById(android.R.id.content),
"更新已下载,重启以完成安装",
Snackbar.LENGTH_INDEFINITE
).setAction("重启") {
appUpdateManager.completeUpdate()
}.show()
}
private fun updateProgressUI(progress: Float) { /* 更新进度 UI */ }
private fun showUpdateFailedDialog() { /* 显示失败弹窗 */ }
companion object {
const val UPDATE_REQUEST_CODE = 1001
}
}
Demo 代码:chapter09
kotlin
复制代码
// chapter09/SystemFeaturesDemo.kt
package com.example.androiddemos.chapter09
import android.Manifest
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun Chapter09SystemFeaturesDemo() {
LazyColumn(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item { Text("系统能力 Demo", style = MaterialTheme.typography.headlineSmall) }
item { PermissionDemoCard() }
item { NotificationDemoCard() }
item { LocationDemoCard() }
item { BiometricDemoCard() }
}
}
@Composable
private fun PermissionDemoCard() {
var permissionStatus by remember { mutableStateOf("未请求") }
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Security, null, modifier = Modifier.size(24.dp))
Spacer(Modifier.width(8.dp))
Text("运行时权限", style = MaterialTheme.typography.titleMedium)
}
Spacer(Modifier.height(12.dp))
Text("状态:$permissionStatus", style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { permissionStatus = "相机权限:已请求(查看说明)" }) {
Text("相机权限")
}
OutlinedButton(onClick = { permissionStatus = "位置权限:已请求(查看说明)" }) {
Text("位置权限")
}
}
}
}
}
@Composable
private fun NotificationDemoCard() {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Notifications, null, modifier = Modifier.size(24.dp))
Spacer(Modifier.width(8.dp))
Text("推送通知", style = MaterialTheme.typography.titleMedium)
}
Spacer(Modifier.height(8.dp))
Text("FCM Token: xxxx...(已注册)", style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.height(8.dp))
val channels = listOf("通用通知", "订单通知", "消息通知")
channels.forEach { channel ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(channel, style = MaterialTheme.typography.bodyMedium)
Text("已创建", style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary)
}
}
}
}
}
@Composable
private fun LocationDemoCard() {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.LocationOn, null, modifier = Modifier.size(24.dp))
Spacer(Modifier.width(8.dp))
Text("位置服务", style = MaterialTheme.typography.titleMedium)
}
Spacer(Modifier.height(8.dp))
Text("纬度: 39.908800", style = MaterialTheme.typography.bodyMedium)
Text("经度: 116.397400", style = MaterialTheme.typography.bodyMedium)
Text("地址: 北京市东城区天安门广场", style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.height(8.dp))
Text("精度: 15.0m | 速度: 0.0 m/s",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
@Composable
private fun BiometricDemoCard() {
var authStatus by remember { mutableStateOf("待验证") }
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Fingerprint, null, modifier = Modifier.size(24.dp))
Spacer(Modifier.width(8.dp))
Text("生物识别", style = MaterialTheme.typography.titleMedium)
}
Spacer(Modifier.height(8.dp))
Text("状态:$authStatus")
Spacer(Modifier.height(8.dp))
Button(onClick = {
authStatus = "✅ 认证成功(Demo:真实环境需要 Activity 上下文)"
}) { Text("触发生物识别验证") }
}
}
}
章节总结
| 知识点 |
必掌握程度 |
面试频率 |
| 运行时权限请求(Launcher) |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| 权限永久拒绝引导设置 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| FCM 推送集成 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| 通知渠道(NotificationChannel) |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| CameraX 基础使用 |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| FusedLocationProvider |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| BiometricPrompt |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| WorkManager 约束+链式任务 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| 应用内更新(In-App Update) |
⭐⭐⭐ |
⭐⭐⭐ |