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" />
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);
}
- 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();
}
}
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();
}
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
| Permission | Why RecipeBox needs it | Notes |
|---|---|---|
| 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.