Runtime Permissions

Since Android 6.0 (API 23), sensitive permissions – camera, location, contacts, and others classified as dangerous – can't just be declared in the manifest and forgotten. The app must also ask the user for them at runtime, while it's actually running, and the user can grant or deny each one individually. RecipeBox needs this for its "photograph your dish" feature, which requires camera access.

Declaring the Permission

Every dangerous permission still needs a manifest declaration – that's step one, not the whole story:

<uses-permission android:name="android.permission.CAMERA" />
Declaring it in the manifest tells the Play Store and the OS that the app may use the camera. It does not grant access by itself – the user must still approve it at runtime.

Checking If It's Already Granted

boolean hasCameraPermission = ContextCompat.checkSelfPermission(
        this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;

if (hasCameraPermission) {
    openCameraForRecipePhoto();
} else {
    requestCameraPermission(); // shown below
}

Requesting a Permission: The Modern Way

The recommended approach for new code is the ActivityResultContracts.RequestPermission() contract, registered once, with the result delivered to a callback – no more overriding a separate method by hand:

// Register once, e.g. as a field in the Activity/Fragment
private final ActivityResultLauncher<String> cameraPermissionLauncher =
        registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
            if (granted) {
                openCameraForRecipePhoto();
            } else {
                Toast.makeText(this,
                        "Camera permission is needed to photograph your dish",
                        Toast.LENGTH_SHORT).show();
            }
        });

private void requestCameraPermission() {
    cameraPermissionLauncher.launch(Manifest.permission.CAMERA);
}
Explanation:
  • registerForActivityResult() must be called unconditionally, before the Activity/Fragment reaches STARTED – typically as a field initializer.
  • The lambda receives a simple boolean: granted or denied.
  • launcher.launch(...) is what actually shows the system permission dialog.

The Older Callback Pattern (Legacy Code)

Before the Activity Result API existed, permissions were requested with ActivityCompat.requestPermissions() and handled in an overridden onRequestPermissionsResult() method:

// Legacy pattern – you will still see this in older codebases
ActivityCompat.requestPermissions(this,
        new String[]{ Manifest.permission.CAMERA }, 100);

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == 100 && grantResults.length > 0
            && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        openCameraForRecipePhoto();
    }
}
Recommendation: use the modern ActivityResultContracts API shown above for all new code – it avoids manually matching request codes and keeps the request and its result next to each other instead of split across two methods.

Explaining Why You Need It (Rationale)

If the user denies a permission once, it's good practice to explain why RecipeBox needs it before asking again, rather than just re-showing the same system dialog:

if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
    // Show your own explanation UI (dialog/snackbar) first
    new AlertDialog.Builder(this)
            .setMessage("RecipeBox needs camera access to let you photograph your dish.")
            .setPositiveButton("OK", (dialog, which) -> requestCameraPermission())
            .show();
} else {
    requestCameraPermission();
}
Explanation: shouldShowRequestPermissionRationale() returns true only after a prior denial (and before a "don't ask again" style permanent denial) – it's Android's built-in signal that an explanation would help.

Requesting Multiple Permissions at Once

If a screen needs more than one permission together, use RequestMultiplePermissions() instead of requesting one at a time:

private final ActivityResultLauncher<String[]> multiplePermissionsLauncher =
        registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), results -> {
            boolean cameraGranted = Boolean.TRUE.equals(results.get(Manifest.permission.CAMERA));
            boolean locationGranted = Boolean.TRUE.equals(results.get(Manifest.permission.ACCESS_FINE_LOCATION));
            // React to each permission's result independently
        });

multiplePermissionsLauncher.launch(new String[]{
        Manifest.permission.CAMERA,
        Manifest.permission.ACCESS_FINE_LOCATION
});

Reference: RecipeBox's Permissions

PermissionWhy RecipeBox needs itNotes
CAMERA Photographing a finished dish for a recipe's photoUri Dangerous permission since API 23; request just before opening the camera
ACCESS_FINE_LOCATION Finding nearby grocery stores for missing ingredients Ask only when the feature is actually used, not at app launch
POST_NOTIFICATIONS Showing sync/reminder notifications (see Notifications & Channels) Became a runtime-requestable permission starting API 33 (Android 13)

Troubleshooting

App crashes with "Fragment... not attached" on the launcher? → Make sure registerForActivityResult() is called as an unconditional field initializer, not inside a click listener or conditional branch.

Permission dialog never shows again after one denial? → The user may have selected "Don't ask again"; at that point, send them to app settings instead of re-requesting.

Feature silently doesn't work after granting permission? → Double-check you're calling the feature (e.g. opening the camera) inside the granted branch of the callback, not before the permission result arrives.

With permissions covered, continue to App Signing & Keystore to learn how RecipeBox gets signed and prepared for release.