πŸ€– Unit 13 β€’ Android Mobile Engineering

Permissions & Device Features: Hardware & Security

Architecting secure, user-centric hardware integrations in modern Android: The dynamic runtime permission state machine, Activity Result API contracts, CameraX lifecycle binding, Fused Location Provider geocoding, Scoped Storage & Photo Picker, POST_NOTIFICATIONS (API 33+), hardware SensorManager pipelines, inter-app sharing intents, and Bluetooth Low Energy (BLE) scanning.

πŸ›‘οΈ Security: Runtime Permissions & Rationale
πŸ“· Hardware: CameraX & Media Sensors
πŸ“ Location: FusedLocationProviderClient
πŸ“ Storage: Scoped MediaStore & PhotoPicker
01

Android Permission Security Architecture & Runtime State Machine

πŸ”’ The Evolution of Android Permission Protection Levels

The Android OS secures sensitive device resources, user privacy, and hardware peripherals through a multi-tiered permission subsystem. Permissions fall into three fundamental protection levels:

  • Normal Permissions: Minimal risk to user privacy or system operation (e.g. INTERNET, ACCESS_NETWORK_STATE, SET_WALLPAPER). Granted automatically by the OS at install time without prompting the user.
  • Dangerous (Runtime) Permissions: Provide access to private user data or sensitive hardware peripherals (e.g. CAMERA, ACCESS_FINE_LOCATION, RECORD_AUDIO, READ_CONTACTS). Since Android 6.0 (API 23), apps must request these permissions at runtime when the feature is needed.
  • Special / Signature Permissions: System-level permissions requiring user intervention in system settings (e.g. SYSTEM_ALERT_WINDOW for draw-over-other-apps, MANAGE_EXTERNAL_STORAGE for all-files access, or signature-matched OEM OEM permissions).
πŸ”„ The Runtime Permission Decision Workflow

Modern Android applications must follow a strictly governed 4-step permission request protocol:

  1. Check Status: Call ContextCompat.checkSelfPermission(context, permission). If it returns PackageManager.PERMISSION_GRANTED, immediately execute the hardware action.
  2. Evaluate Rationale: If denied, call ActivityCompat.shouldShowRequestPermissionRationale(activity, permission). If true, the user previously denied the permission without checking "Never ask again". The app MUST render an educational dialog explaining why the hardware feature is required before re-prompting.
  3. Launch System Dialog: Use the modern Activity Result API (specifically ActivityResultContracts.RequestPermission() or RequestMultiplePermissions()). The legacy requestPermissions() and onRequestPermissionsResult() callbacks are deprecated.
  4. Permanent Denial ("Never Ask Again") Recovery: If the user selects "Don't ask again" (or denies twice on Android 11+), the OS suppresses the system prompt. The app must detect this and provide a direct navigation button to the app's system settings via Settings.ACTION_APPLICATION_DETAILS_SETTINGS.
Visual Architecture: Android Runtime Permission State Machine
User Action Tap Camera / GPS button
βž”
checkSelfPermission() Inspect Android PackageManager
⬇ Granted?
PERMISSION_GRANTED Execute Hardware Feature
⇦ (Yes) | (No) ⇨
shouldShowRationale()? Inspect user history
⬇ True? (Explain) / False? (Direct prompt)
Show Custom Explainer UI Dialog explaining feature value
βž”
ActivityResultContract RequestPermission() prompt
βž”
Permanent Denial Route to Settings Intent
02

Camera Integration & Modern Jetpack CameraX

πŸ“· Why CameraX over Legacy Camera & Camera2?

The original android.hardware.Camera API was deprecated for its rigid architecture, while the lower-level Camera2 API introduced in Android 5.0 (Lollipop) required hundreds of lines of boilerplate to manage sensor streams, surface configurations, and hardware quirks across thousands of fragmented OEM devices.

Jetpack CameraX: Google's modern, opinionated, lifecycle-aware camera abstraction layer that sits on top of Camera2. CameraX eliminates OEM-specific edge cases (such as aspect ratio stretching, rotation quirks, and HDR crashes) through internal Google-tested device compatibility databases.

  • Preview Use Case: Streams live sensor feed directly to an on-screen Compose AndroidView(PreviewView).
  • ImageCapture Use Case: Captures high-resolution JPEG photos to memory or file storage with hardware flash, auto-focus, and rotation handling.
  • ImageAnalysis Use Case: Provides a continuous buffer of non-blocking frames (YUV_420_888 format) for real-time ML Kit processing (e.g. barcode/QR code scanning, face detection, OCR).
πŸ› οΈ Lifecycle-Aware Camera Binding
CameraHelper.kt Kotlin
suspend fun bindCameraPreview(
    lifecycleOwner: LifecycleOwner,
    previewView: PreviewView,
    context: Context
): ImageCapture = suspendCoroutine { continuation ->
    val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

    cameraProviderFuture.addListener({
        val cameraProvider = cameraProviderFuture.get()

        // 1. Configure the Preview use case
        val preview = Preview.Builder().build().also {
            it.setSurfaceProvider(previewView.surfaceProvider)
        }

        // 2. Configure high-resolution ImageCapture use case
        val imageCapture = ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
            .build()

        // 3. Select back-facing lens
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        try {
            // Unbind any active use cases and bind to Activity/Fragment Lifecycle
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(
                lifecycleOwner,
                cameraSelector,
                preview,
                imageCapture
            )
            continuation.resume(imageCapture)
        } catch (exc: Exception) {
            continuation.resumeWithException(exc)
        }
    }, ContextCompat.getMainExecutor(context))
}
03

Location Services, GPS & Google Maps

πŸ“ The Modern Geolocation Subsystem: FusedLocationProviderClient

Accessing raw GPS hardware chips directly via Android's legacy LocationManager is notoriously battery-expensive and slow (often requiring up to 30 seconds for a cold GPS satellite lock). Google Play Services provides the FusedLocationProviderClient, which seamlessly combines signals from:

  • GPS Satellites: Precise outdoor triangulation (accuracy < 5 meters, high power).
  • Wi-Fi Access Points: SSID signal RSSI mapping (indoor & urban accuracy 10–50m, low power).
  • Cellular Base Stations: Cell tower ID triangulation (accuracy 500–2000m, negligible power).
  • Inertial Device Sensors: Accelerometers and gyroscopes for dead-reckoning between GPS fixes.
Permission Resolution Accuracy Target Hardware Sensors Required Battery Impact
ACCESS_COARSE_LOCATION City block / ~3 km radius (Android 12+ approximate location). Cell tower triangulation, coarse Wi-Fi scanning. Very Low (Battery friendly).
ACCESS_FINE_LOCATION Precise coordinates (within 3 to 10 meters). Dedicated GPS chip + Galileo + GLONASS + Wi-Fi. High (Substantial battery consumption if streaming).
ACCESS_BACKGROUND_LOCATION Periodic geofencing / tracking while app is closed. Hardware geofencing subsystem / Cell towers. Restricted; requires separate standalone user grant in Settings.
πŸ—ΊοΈ Requesting Periodic GPS Coordinate Updates
LocationTracker.kt Kotlin
class DevAscentLocationManager(private val context: Context) {
    private val fusedClient = LocationServices.getFusedLocationProviderClient(context)

    @SuppressLint("MissingPermission")
    fun subscribeLocationUpdates(onLocation: (Location) -> Unit) {
        val request = LocationRequest.Builder(
            Priority.PRIORITY_HIGH_ACCURACY,
            10000L // Interval: 10 seconds
        )
            .setMinUpdateIntervalMillis(5000L) // Fastest rate: 5 seconds
            .setMinUpdateDistanceMeters(10f) // Minimum displacement: 10 meters
            .build()

        val callback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                result.lastLocation?.let(onLocation)
            }
        }

        fusedClient.requestLocationUpdates(request, callback, Looper.getMainLooper())
    }
}
04

Storage Access: Scoped Storage, MediaStore & The Zero-Permission Photo Picker

πŸ›‘ The Deprecation of Broad External Storage Permissions

Prior to Android 10 (API 29), requesting READ_EXTERNAL_STORAGE granted an application unrestricted access to the entire /sdcard/ file system, allowing malicious apps to inspect sensitive documents, photos, and databases generated by other applications. Google introduced Scoped Storage to isolate apps into sandboxed boundaries:

  • App-Specific Internal Sandbox (context.filesDir): Strictly private to the application. Automatically deleted upon uninstallation. Requires ZERO permissions.
  • Shared Media Collections (MediaStore): System-managed shared collections for Images, Video, and Audio. Apps can write their own files without permission, but require granular permissions to read media created by other apps.
✨ Android 13 Granular Media & The Android Photo Picker

In Android 13 (API 33), Google eliminated READ_EXTERNAL_STORAGE and replaced it with granular media permissions:

  • android.permission.READ_MEDIA_IMAGES (Photos, screenshots, diagrams)
  • android.permission.READ_MEDIA_VIDEO (Recorded camera footage, downloaded MP4s)
  • android.permission.READ_MEDIA_AUDIO (Podcasts, voice notes, songs)

The Revolutionary Android Photo Picker: Introduced via Google Play System updates for Android 11+ (and backported to Android 4.4+ via Google Play Services), the Photo Picker lets users select specific photos or videos to share with an app without granting ANY storage or media permissions whatsoever!

PhotoPickerIntegration.kt Kotlin / Compose
@Composable
fun ProfileAvatarPicker(onImageSelected: (Uri) -> Unit) {
    // ZERO PERMISSIONS REQUIRED! Works out of the box on Android 11+
    val pickerLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.PickVisualMedia()
    ) { uri: Uri? ->
        uri?.let(onImageSelected)
    }

    Button(onClick = {
        pickerLauncher.launch(
            PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
        )
    }) {
        Text("Select Profile Photo (Zero Permission)")
    }
}
05

Hardware Sensors Framework: Accelerometers, Gyroscopes & Power Hygiene

🧭 The SensorManager & SensorEventListener Pipeline

Android devices pack an array of micro-electro-mechanical (MEMS) sensors to measure motion, orientation, and ambient environmental conditions. The architecture centers around:

  • SensorManager: System service used to discover available sensors, register listeners, and configure sampling delays.
  • Sensor: Representation of a specific hardware sensor (e.g. TYPE_ACCELEROMETER, TYPE_GYROSCOPE, TYPE_LIGHT, TYPE_PROXIMITY).
  • SensorEventListener: Callback interface that receives SensorEvent objects containing raw floating-point sensor values (e.g. m/sΒ² acceleration across X, Y, Z axes).

Critical Battery Rule: Sensor chips consume significant battery and keep the CPU awake. Applications MUST register sensor listeners in onResume() and always unregister them in onPause() to prevent severe background battery drain.

ShakeDetector.kt Kotlin
class ShakeDetector(
    context: Context,
    private val onShake: () -> Unit
) : SensorEventListener {

    private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    fun start() {
        accelerometer?.let {
            sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI)
        }
    }

    fun stop() {
        sensorManager.unregisterListener(this) // Mandatory to stop battery drain!
    }

    override fun onSensorChanged(event: SensorEvent?) {
        event?.let {
            val x = it.values[0]
            val y = it.values[1]
            val z = it.values[2]
            val acceleration = Math.sqrt((x * x + y * y + z * z).toDouble()) - SensorManager.GRAVITY_EARTH
            if (acceleration > 12.0) {
                onShake()
            }
        }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
06

Inter-App Communication: Sharing, Calling, Messaging & Bluetooth BLE

πŸ“€ Sharing Content, Phone Calls & SMS Messaging

Instead of reinventing custom SMS or telephony engines, Android encourages Intent-based delegation to established system applications:

  • Content Sharing (Intent.ACTION_SEND): Invokes the native Android Sharesheet via Intent.createChooser(), allowing users to send text, URLs, or file URIs (via FileProvider) to WhatsApp, Slack, Gmail, or Telegram seamlessly.
  • Phone Calls (Intent.ACTION_DIAL): Opens the system phone dialer with a pre-filled telephone number (e.g. tel:+1234567890). Requires NO permissions because the user must explicitly press the call button. (Contrasts with ACTION_CALL, which places the call directly and requires dangerous CALL_PHONE permission).
  • SMS Messaging (Intent.ACTION_SENDTO): Opens the messaging app with the recipient number and pre-drafted message using the smsto: URI scheme without requiring SEND_SMS permission.
IntentDelegator.kt Kotlin
object DeviceIntentHelper {
    fun shareText(context: Context, text: String) {
        val sendIntent = Intent().apply {
            action = Intent.ACTION_SEND
            putExtra(Intent.EXTRA_TEXT, text)
            type = "text/plain"
        }
        context.startActivity(Intent.createChooser(sendIntent, "Share DevAscent Notes Via:"))
    }

    fun openDialer(context: Context, phoneNumber: String) {
        val dialIntent = Intent(Intent.ACTION_DIAL).apply {
            data = Uri.parse("tel:$phoneNumber")
        }
        context.startActivity(dialIntent)
    }
}
πŸ“Ά Bluetooth Low Energy (BLE) Fundamentals (Android 12+ API 31)

Prior to Android 12, Bluetooth scanning required fine location permission because beacon detection could infer physical location. In Android 12+, Google separated location from Bluetooth with dedicated runtime permissions:

  • android.permission.BLUETOOTH_SCAN: Discover nearby BLE peripherals. (Add android:usesPermissionFlags="neverForLocation" if not deriving GPS location).
  • android.permission.BLUETOOTH_CONNECT: Communicate with paired Bluetooth devices.
  • android.permission.BLUETOOTH_ADVERTISE: Broadcast device presence as a peripheral.
07

Production Capstone: Unified Device Hardware & Permissions Manager

The complete, verified production component below demonstrates modern permission handling in Jetpack Compose: requesting multi-runtime permissions (Camera, Location, Notifications), checking rationales, selecting images with Zero-Permission PhotoPicker, and triggering system intents.

DevAscentDeviceHub.kt Kotlin / Jetpack Compose
package me.devascent.academy.device

import android.Manifest
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp

@Composable
fun DevAscentDeviceHubScreen() {
    val context = LocalContext.current
    var statusMessage by remember { mutableStateOf("Ready for hardware operations.") }
    var selectedImageUri by remember { mutableStateOf<Uri?>(null) }

    // 1. Multi-Permission Launcher (Camera + Location + Android 13 Notifications)
    val permissionsToRequest = remember {
        val list = mutableListOf(
            Manifest.permission.CAMERA,
            Manifest.permission.ACCESS_FINE_LOCATION
        )
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            list.add(Manifest.permission.POST_NOTIFICATIONS)
        }
        list.toTypedArray()
    }

    val multiPermissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestMultiplePermissions()
    ) { results ->
        val cameraGranted = results[Manifest.permission.CAMERA] == true
        val locationGranted = results[Manifest.permission.ACCESS_FINE_LOCATION] == true

        statusMessage = when {
            cameraGranted && locationGranted -> "Camera & GPS Granted! Initializing hardware..."
            !cameraGranted && !locationGranted -> "Permissions Denied. Please enable in Settings."
            else -> "Partial access granted. Camera: $cameraGranted, GPS: $locationGranted"
        }
    }

    // 2. Zero-Permission PhotoPicker Launcher
    val photoPickerLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.PickVisualMedia()
    ) { uri: Uri? ->
        selectedImageUri = uri
        statusMessage = uri?.let { "Photo picked: ${it.lastPathSegment}" } ?: "Selection cancelled."
    }

    Column(
        modifier = Modifier.fillMaxSize().padding(20.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Text(
            text = "Hardware & Security Console",
            style = MaterialTheme.typography.headlineSmall
        )

        Card(modifier = Modifier.fillMaxWidth()) {
            Text(
                text = statusMessage,
                modifier = Modifier.padding(16.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }

        // Action: Request Multi-Permissions
        Button(
            onClick = { multiPermissionLauncher.launch(permissionsToRequest) },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("1. Request Camera & Location Permissions")
        }

        // Action: Launch Zero-Permission Photo Picker
        Button(
            onClick = {
                photoPickerLauncher.launch(
                    PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
                )
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("2. Pick Photo (Zero Permissions)")
        }

        // Action: Native Android Sharesheet Intent
        OutlinedButton(
            onClick = {
                val sendIntent = Intent(Intent.ACTION_SEND).apply {
                    putExtra(Intent.EXTRA_TEXT, "Check out DevAscent Android Engineering Notes!")
                    type = "text/plain"
                }
                context.startActivity(Intent.createChooser(sendIntent, "Share With"))
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("3. Share Content via Intent")
        }

        // Action: Open System App Settings Fallback
        TextButton(
            onClick = {
                val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                    data = Uri.fromParts("package", context.packageName, null)
                }
                context.startActivity(settingsIntent)
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Open Application Details Settings")
        }
    }
}
Simulated Android Studio Logcat Execution Stream
09:20:14.301 I/DevAscentSecurity: Checking permissions: [android.permission.CAMERA, android.permission.ACCESS_FINE_LOCATION] 09:20:14.312 D/ActivityResult: Launching RequestMultiplePermissions contract 09:20:16.850 I/PermissionController: User grant: CAMERA=GRANTED, ACCESS_FINE_LOCATION=GRANTED, POST_NOTIFICATIONS=GRANTED 09:20:16.862 I/DevAscentSecurity: Status: Camera & GPS Granted! Initializing hardware... 09:20:18.104 D/PhotoPicker: Launching PickVisualMedia Activity (Photo Picker Provider: com.google.android.providers.media.photopicker) 09:20:20.440 I/PhotoPicker: Result URI: content://media/picker/0/com.android.providers.media.photopicker/media/1000004 09:20:21.112 D/IntentResolver: Dispatched Intent.ACTION_SEND with Chooser to com.whatsapp / com.slack
08

University Exam & Technical Interview Viva Guide (8 Master Q&As)

Q1. What is the fundamental difference between Normal and Dangerous permissions in Android?
Normal Permissions (e.g. INTERNET, ACCESS_NETWORK_STATE) pose minimal risk to user privacy or device security; the Android OS grants them automatically upon installation without user confirmation. Dangerous Permissions (e.g. CAMERA, ACCESS_FINE_LOCATION, READ_CONTACTS) access private user data or sensitive hardware peripherals; they require explicit runtime consent from the user via system dialog prompts since Android 6.0 (API 23).
Q2. What role does shouldShowRequestPermissionRationale() play in permission UX?
shouldShowRequestPermissionRationale() returns true if the user has previously denied the permission request but has not yet checked "Don't ask again" (or denied twice on Android 11+). When true, the application should display an educational explanation (via dialog or inline card) detailing why the feature cannot function without the permission before triggering the system prompt again. If it returns false on a denied permission, the permission has been permanently denied and must be resolved by directing the user to system settings.
Q3. Why is Jetpack CameraX preferred over the native Camera2 API?
While Camera2 offers low-level sensor control, it requires extensive boilerplate and suffers from OEM-specific hardware inconsistencies (e.g. stretched preview aspect ratios, rotation quirks, device-specific crash bugs). CameraX provides a high-level, lifecycle-aware architecture (Preview, ImageCapture, ImageAnalysis) that automatically resolves hardware fragmentation through Google's automated compatibility test suite.
Q4. How does FusedLocationProviderClient conserve battery compared to legacy LocationManager?
Legacy LocationManager forces apps to directly activate the power-hungry physical GPS satellite chip. FusedLocationProviderClient intelligently fuses data from GPS, Wi-Fi networks, cellular towers, and onboard accelerometers. It supports batching, passive background location sharing across apps, and priority levels (e.g. PRIORITY_BALANCED_POWER_ACCURACY) to achieve fast locks with up to 80% less battery consumption.
Q5. How does the Android Photo Picker provide access to user media with zero permissions?
The Android Photo Picker runs in an isolated system process owned by the OS MediaProvider. The user browses and selects images within this secure system UI. Once selected, the system grants the calling app temporary read access URI permissions exclusively for the chosen files. The app never accesses the broader MediaStore or external storage, maintaining strict user privacy without requesting READ_MEDIA_IMAGES.
Q6. What happens if a sensor listener is not unregistered in onPause()?
If a SensorEventListener is not unregistered when an Activity goes into the background, the hardware sensor remains powered on and actively streams data to the CPU. This prevents the device from entering low-power Doze sleep states, causing rapid battery drain and severe device overheating. Listeners must always be registered in onResume() and unregistered in onPause().
Q7. What is the difference between Intent.ACTION_DIAL and Intent.ACTION_CALL?
Intent.ACTION_DIAL opens the standard phone dialer with a pre-filled number, allowing the user to inspect the number and manually initiate the call. It requires no permissions. Intent.ACTION_CALL initiates a phone call immediately without user intervention; it requires the dangerous runtime permission android.permission.CALL_PHONE and should only be used in specialized dialer applications.
Q8. How did Bluetooth permissions change in Android 12 (API 31)?
In Android 12+, Bluetooth was decoupled from location permissions. Legacy Android required ACCESS_FINE_LOCATION to scan for Bluetooth devices. Android 12 introduced three dedicated runtime permissions: BLUETOOTH_SCAN, BLUETOOTH_CONNECT, and BLUETOOTH_ADVERTISE. Developers can append the attribute android:usesPermissionFlags="neverForLocation" to BLUETOOTH_SCAN to scan for peripherals without needing location permissions.