Working with Permissions in XML-Based Android Projects

In Android development, handling permissions is crucial for accessing sensitive user data or system features. While modern Android development often utilizes Kotlin and Jetpack Compose, many existing and legacy projects are still based on XML. Understanding how to effectively manage permissions in XML-based Android projects remains essential.

What are Android Permissions?

Android permissions are a security mechanism that controls access to sensitive user data and system features. Apps must request permission from the user before accessing certain resources, such as the camera, microphone, location, or contacts. This ensures user privacy and protects the integrity of the system.

Why are Permissions Important?

  • User Privacy: Protects user data and prevents unauthorized access.
  • Security: Ensures that apps only access resources they are authorized to use.
  • User Trust: Transparency in permission requests builds trust with users.

Declaring Permissions in AndroidManifest.xml

In XML-based Android projects, permissions are declared in the AndroidManifest.xml file. The <uses-permission> tag is used to request specific permissions.

Step 1: Open AndroidManifest.xml

Locate the AndroidManifest.xml file in your project’s app/manifests/ directory.

Step 2: Declare Permissions

Add the <uses-permission> tag for each permission your app needs. For example, to request camera and location permissions, add the following:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApp">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

Requesting Permissions at Runtime

Declaring permissions in the manifest is not enough. On Android 6.0 (API level 23) and higher, you must also request dangerous permissions at runtime.

Step 1: Check for Permission

Use ContextCompat.checkSelfPermission() to check if the app already has the permission.

import android.Manifest;
import android.content.pm.PackageManager;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;

public class MainActivity extends AppCompatActivity {

    private static final int CAMERA_PERMISSION_REQUEST_CODE = 100;
    private Button cameraButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        cameraButton = findViewById(R.id.cameraButton);
        cameraButton.setOnClickListener(v -> checkCameraPermission());
    }

    private void checkCameraPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            // Permission is not granted
            requestCameraPermission();
        } else {
            // Permission is already granted
            openCamera();
        }
    }

    private void openCamera() {
        Toast.makeText(this, "Opening Camera", Toast.LENGTH_SHORT).show();
        // Implement camera opening logic here
    }
}

Step 2: Request the Permission

If the permission is not granted, use ActivityCompat.requestPermissions() to request it.

import android.Manifest;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;

public class MainActivity extends AppCompatActivity {

    private static final int CAMERA_PERMISSION_REQUEST_CODE = 100;
    private Button cameraButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        cameraButton = findViewById(R.id.cameraButton);
        cameraButton.setOnClickListener(v -> checkCameraPermission());
    }

    private void checkCameraPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            // Permission is not granted
            requestCameraPermission();
        } else {
            // Permission is already granted
            openCamera();
        }
    }

    private void requestCameraPermission() {
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.CAMERA},
                CAMERA_PERMISSION_REQUEST_CODE);
    }

    private void openCamera() {
        Toast.makeText(this, "Opening Camera", Toast.LENGTH_SHORT).show();
        // Implement camera opening logic here
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission was granted
                openCamera();
            } else {
                // Permission was denied
                Toast.makeText(this, "Camera permission denied", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

Step 3: Handle the Permission Result

Override onRequestPermissionsResult() to handle the result of the permission request.

Best Practices for Handling Permissions

  • Request Permissions Just-In-Time: Only request permissions when they are actually needed.
  • Explain Why Permissions Are Needed: Provide a clear explanation of why your app requires specific permissions.
  • Handle Permission Denials Gracefully: If the user denies a permission, explain the consequences and provide alternative ways to use the app without that permission.
  • Test Permission Scenarios: Thoroughly test your app’s behavior under different permission scenarios, including granted, denied, and revoked permissions.

Example: Accessing Location

Here’s a complete example of how to access the device’s location using XML and Java, including declaring the necessary permissions, requesting them at runtime, and handling the results.

AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <TextView
        android:id="@+id/locationTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Location: Not Available"/>

    <Button
        android:id="@+id/getLocationButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/locationTextView"
        android:text="Get Location"/>

</RelativeLayout>

MainActivity.java:

import android.Manifest;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

public class MainActivity extends AppCompatActivity {

    private static final int LOCATION_PERMISSION_REQUEST_CODE = 200;
    private TextView locationTextView;
    private Button getLocationButton;
    private LocationManager locationManager;
    private LocationListener locationListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        locationTextView = findViewById(R.id.locationTextView);
        getLocationButton = findViewById(R.id.getLocationButton);

        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        locationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                locationTextView.setText("Location: " + location.getLatitude() + ", " + location.getLongitude());
            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {
            }

            @Override
            public void onProviderEnabled(String provider) {
            }

            @Override
            public void onProviderDisabled(String provider) {
                Toast.makeText(MainActivity.this, "Please enable location service", Toast.LENGTH_SHORT).show();
            }
        };

        getLocationButton.setOnClickListener(v -> checkLocationPermission());
    }

    private void checkLocationPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                    LOCATION_PERMISSION_REQUEST_CODE);
        } else {
            getLocation();
        }
    }

    private void getLocation() {
        try {
            locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener);
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                getLocation();
            } else {
                Toast.makeText(this, "Location permission denied", Toast.LENGTH_SHORT).show();
            }
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        locationManager.removeUpdates(locationListener);
    }
}

This example includes:

  • Declaring the ACCESS_FINE_LOCATION permission in AndroidManifest.xml.
  • Checking for the location permission at runtime using ContextCompat.checkSelfPermission().
  • Requesting the permission using ActivityCompat.requestPermissions().
  • Handling the permission result in onRequestPermissionsResult().
  • Accessing the location using LocationManager and LocationListener, but only after the permission has been granted.
  • Handling the case where the user denies the permission by displaying a toast message.
  • Properly unregistering the location listener in onPause() to prevent memory leaks and unnecessary battery consumption.

Conclusion

Effectively managing permissions is crucial in Android development to ensure user privacy and security. By declaring permissions in AndroidManifest.xml and requesting dangerous permissions at runtime, you can build robust and user-friendly applications. Always follow best practices to provide clear explanations to users and handle permission denials gracefully.