第九章:安卓系统能力与平台集成

本章深入 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) ⭐⭐⭐ ⭐⭐⭐
相关推荐
moonsims2 小时前
基于AiBrainBox-UGV的Smart RoBot系统架构&多Smart Robot协同架构:数据流 + 多机协同架构图
人工智能·数码相机·无人机
阿拉斯攀登2 小时前
20 个 Android JNI + CMake 生产级示例
android·java·开发语言·人工智能·机器学习·无人售货柜
空中海2 小时前
第十一章:Kotlin 进阶与 Android 原理
android
码农的日常搅屎棍2 小时前
视觉标定--眼在手上整相机标定步骤适配随机工作平面
人工智能·数码相机·计算机视觉
studyForMokey2 小时前
【Android面试】设计模式专题
android·设计模式·面试
三少爷的鞋2 小时前
别再写 BaseXXX 了:BaseActivity 和 BaseViewModel 正在毁掉你的架构
android
三维频道2 小时前
精密功能主义:DIC全场变形检测的系统秩序与物理真实
数码相机·机器人·xtdic·精密功能主义·光学测试装备·微距形变分析·机器视觉应用
gaosushexiangji2 小时前
基于sCMOS相机的冷离子云成像与量子测量实验研究
数码相机
Trustport2 小时前
ArcGIS Maps SDK For Kotlin 加载Layout中的MapView出错
android·开发语言·arcgis·kotlin