Permissions & Best Practices
Every Android app runs inside a sandboxed Linux process. It cannot read another app's files, open a network socket, or access the camera without first declaring and — for dangerous permissions — explicitly requesting user consent at runtime. This lesson covers the full lifecycle of Android permissions and the secure-data-handling patterns that separate production-quality apps from toy projects.
Two Tiers of Permissions
Android divides permissions into two broad categories:
- Normal permissions — low-risk capabilities (e.g.,
INTERNET, ACCESS_NETWORK_STATE). The system grants these automatically at install time; you only need to declare them in AndroidManifest.xml.
- Dangerous permissions — access to sensitive user data or hardware (e.g.,
READ_CONTACTS, CAMERA, ACCESS_FINE_LOCATION). These must be declared and requested at runtime, and the user can revoke them at any time from Settings.
Always declare every permission you use in the manifest, even normal ones. Skipping the manifest declaration causes an immediate SecurityException at runtime, regardless of whether the user has granted it.
Declaring Permissions in the Manifest
Open AndroidManifest.xml and add <uses-permission> tags as direct children of the root <manifest> element:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<!-- Normal: granted at install, no runtime dialog -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Dangerous: must also be requested at runtime -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application ...>
...
</application>
</manifest>
The Runtime Permission Flow
For dangerous permissions the pattern is always: check → request → handle result. Never assume a permission is granted between app launches — the user can revoke it at any moment.
The modern way to handle this is with the Activity Result API (ActivityResultLauncher), available from androidx.activity:activity:1.2+. It replaces the old onRequestPermissionsResult callback and eliminates boilerplate.
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
public class ContactsActivity extends AppCompatActivity {
// Register the launcher BEFORE onCreate (field-level declaration)
private final ActivityResultLauncher<String> requestPermissionLauncher =
registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (isGranted) {
loadContacts(); // user said yes
} else {
showPermissionRationale(); // user said no
}
}
);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contacts);
checkAndRequestContactsPermission();
}
private void checkAndRequestContactsPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED) {
loadContacts(); // already have it
} else if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
// User previously denied; explain WHY before asking again
showRationaleDialog();
} else {
requestPermissionLauncher.launch(Manifest.permission.READ_CONTACTS);
}
}
private void loadContacts() {
Toast.makeText(this, "Loading contacts...", Toast.LENGTH_SHORT).show();
// ... query ContentResolver
}
private void showPermissionRationale() {
Toast.makeText(this,
"Contacts permission is required to show your connections.",
Toast.LENGTH_LONG).show();
}
private void showRationaleDialog() {
// Show an AlertDialog explaining the need, then launch on confirm
new androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("Permission needed")
.setMessage("This app reads your contacts to suggest connections.")
.setPositiveButton("OK", (d, w) ->
requestPermissionLauncher.launch(Manifest.permission.READ_CONTACTS))
.setNegativeButton("Cancel", null)
.show();
}
}
Always check shouldShowRequestPermissionRationale(). If it returns true, the user denied once before — show an in-context explanation before requesting again. Launching the system dialog cold a second time without an explanation is a common UX mistake that leads to permanent denial.
Requesting Multiple Permissions at Once
When your feature needs several dangerous permissions simultaneously (e.g., both CAMERA and RECORD_AUDIO for a video call), use RequestMultiplePermissions:
private final ActivityResultLauncher<String[]> multiPermissionLauncher =
registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
results -> {
Boolean cameraGranted = results.getOrDefault(Manifest.permission.CAMERA, false);
Boolean audioGranted = results.getOrDefault(Manifest.permission.RECORD_AUDIO, false);
if (Boolean.TRUE.equals(cameraGranted) && Boolean.TRUE.equals(audioGranted)) {
startVideoCall();
} else {
Toast.makeText(this,
"Both camera and microphone are needed for video calls.",
Toast.LENGTH_LONG).show();
}
}
);
// Trigger it:
multiPermissionLauncher.launch(new String[]{
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
});
Graceful Degradation — Designing for Denial
Your app must remain usable when a permission is denied. Design every feature with a fallback:
- A maps feature without
ACCESS_FINE_LOCATION should let the user type an address instead of auto-locating.
- A contacts-import feature without
READ_CONTACTS should offer a manual-entry form.
- Never call the API that requires the permission without first checking — a
SecurityException will crash your app.
Do not ask for permissions you do not need. Requesting permissions beyond your feature's actual needs damages user trust and may get your app rejected from the Play Store's permission policy review. Audit your manifest before every release.
Secure Data Handling on Device
Permissions protect inter-app access, but you also need to protect data within your own app. Three principles:
1. Never Store Secrets in SharedPreferences
SharedPreferences XML files live in /data/data/<package>/shared_prefs/ and are readable by root and by any process that exploits a vulnerability. Do not store passwords, API keys, or auth tokens there in plaintext. Instead use the Jetpack Security library (EncryptedSharedPreferences), which wraps an AES-256 master key stored in the Android Keystore:
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;
MasterKey masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
SharedPreferences securePrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs", // file name
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
// Use exactly like regular SharedPreferences
securePrefs.edit().putString("auth_token", token).apply();
String token = securePrefs.getString("auth_token", null);
2. Avoid Logging Sensitive Data
Android's logcat is readable by any app with READ_LOGS (and by anyone with USB access). Scrub sensitive values from log calls before release:
// BAD — token visible in logcat
Log.d("Auth", "User token: " + authToken);
// GOOD — gate on BuildConfig
if (BuildConfig.DEBUG) {
Log.d("Auth", "Token acquired (length=" + authToken.length() + ")");
}
Consider using ProGuard/R8 rules to strip all Log.d and Log.v calls from the release build entirely.
3. Use HTTPS and Certificate Validation
Since Android 9 (API 28), cleartext HTTP traffic is blocked by default. All network calls must use HTTPS. If you need to connect to a server with a self-signed certificate during testing, configure a network_security_config.xml rather than disabling all certificate checking — disabling it entirely is a critical security flaw:
<!-- res/xml/network_security_config.xml (for DEBUG builds only) -->
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
<!-- In AndroidManifest.xml, APPLICATION tag only -->
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
In production, never override certificate validation. Trust only your own CA or use certificate pinning (OkHttp's CertificatePinner) for high-value endpoints like authentication and payment.
Summary
Android's permission system is a two-step process: declare in the manifest, then check and request at runtime for dangerous permissions. Use the ActivityResultLauncher API for clean, lifecycle-safe request flows. Always handle denial gracefully — your app should degrade, not crash. Protect on-device data with EncryptedSharedPreferences for secrets, keep logs clean in release builds, and enforce HTTPS everywhere. These practices are not optional extras; they are required for Play Store publication and for earning user trust.