Android Working with Runtime Permission

Every Android application is started in its own process thus are isolated from all other applications (even from system/default applications). As a result, an Android application can’t access any file or data outside its scope until and unless the file or data is shared with the application.

So, the conclusion is that if an application needs anything outside its scope, then it has to request the appropriate permission.

Get Github code from HERE. 

Android Runtime Permissions

  • Runtime permissions are a new feature in Android 6.0 Marshmallow that allow you to request permissions at runtime, when it matters rather than at install time.
  • This will give the user to have more control over the permissions and ensure more security for his/her confidential data.
  • In the case we try to call some function that requires a permission which user has not granted yet, the function will suddenly throw an Exception (java.lang.SecurityException) that will lead to the application crashing. Hence we need to implement this new android permissions model in our application.

Permission Types

Permissions are divided into several protection levels. The two most important protection levels are normal and dangerous permissions.

  • Normal Permissions
  • Dangerous Permissions

Normal Permissions:  Permissions that have very little or no effect on user’s privacy or confidential data are categorized as normal permissions. The system itself grants normal permissions when requested instead of prompting to the user. Examples would be ACCESS_WIFI_STATE, WAKE_LOCK, INTERNETetc.

Dangerous Permissions:  Permissions that may have a greater effect on user’s privacy or confidential data are categorized as dangerous permissions. Examples would be  READ_CONTACTS, ACCESS_FINE_LOCATION, CAMERA, etc. If an app requests dangerous permissions then the user has to explicitly grant permission to the app.

Here is the detailed information about permissions.

Permission Workflow before and from M (API 23)

The permission workflow has been changed from Android Marshmallow(API 23). The way Android asks the user to grant dangerous permissions depends on the version of Android running on the user’s device, and the system version targeted by your app.

 Permission Model before M (API 23): 

  • Before API 23, the permission model was simpler to the developer but offered less control and security to the user.
  • If the device is running Android 5.1.1 (API level 22) or lower, or the app’s targetSdkVersion is 22 or lower while running on any version of Android, the system automatically asks the user to grant all dangerous permissions for your app at install-time.
Screenshot 2019-10-22 19.58.41
Install-time permission dialog
  • If the user clicks Accept, all permissions the app requests are granted. If the user denies the permissions request, the system cancels the installation of the app.
  • Permissions cannot be denied or granted after the installation.
  • Thus developers were required only to declare all needed permissions in the manifest and then just forget, if the app is running then it has all the requested permissions.

Permission Model from M (API 23): 

  • With Android 6.0 Marshmallow (API 23), a new runtime permission model has been introduced.
  • According to this model if the device is running Android 6.0 (API level 23) or higher, and the app’s targetSdkVersion is 23 or higher, the user isn’t notified of any app permissions at install time.
  • Your app must ask the user to grant the dangerous permissions at runtime before performing any action that may require the particular permission.
  • When your app requests permission, the user sees a system dialog which includes a Deny and Allow button.
  • If the user denies the permission request, the next time your app requests the permission, the dialog contains a checkbox that, when checked, indicates the user doesn’t want to be prompted for the permission again.

Screenshot_1571749993        Screenshot_1571750449

Note: According to Google, beginning with Android 6.0 (API level 23), users can revoke permissions from any app at any time, even if the app targets a lower API level.

Check for permission

If your app needs a dangerous permission, then you must check whether you have that permission or not every time when the app performs an operation that requires that permission.

For example, if your app uses the camera feature, then you have to check for the camera permission every time when app opens the camera.

To check if you have a permission, call the checkSelfPermission() method. For example, this snippet shows how to check if the activity has permission to access the camera and location:

private boolean checkPermission() {

//Assume thisActivity as current activity
    int result1 = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.ACCESS_FINE_LOCATION);
    int result2 = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.CAMERA);

    return result1 == PackageManager.PERMISSION_GRANTED && result2 == PackageManager.PERMISSION_GRANTED;
}

If the app has permission, then the method returns packageManager.PERMISSION_GRANTED (ie. 0) and the app can continue with the operation.

If the app does not have permission, then the method returns packageManager.PERMISSION_DENIED (ie. -1) and the app have to explicitly ask the user for permission.

Explain why the app needs permissions

In some circumstances, you want to help the user understand why your app needs a permission.

For example, if you write a camera app, requesting the camera permission would be expected by the user and no rationale for why it is requested is needed. If however, the app needs location for tagging photos then a non-tech savvy user may wonder how location is related to taking photos. In this case, you may choose to show UI with rationale of requesting this permission.

Android provides a utility method, shouldShowRequestPermissionRationale(), that returns true if the user has previously denied the request, and returns false if a user has denied a permission and selected the Don’t ask again option in the permission request dialog, or if a device policy prohibits the permission.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

//shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION) || 
shouldShowRequestPermissionRationale(CAMERA) returns true if 
the user has previously denied any of the request
    if (shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION) || shouldShowRequestPermissionRationale(CAMERA)) {

        showMessageGrantCancel("You need to allow access to both the permissions",
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                   
//when the user click on grant button then 
requestPermission() will call requesting array of permissions.   
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                            requestPermissions(new String[]{ ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, CAMERA}, PERMISSION_REQUEST_CODE);
                        }
                    }
                });
    } 
}

//showMessageGrantCancel() will show an alert dialog with rationale of 
requesting this permissions
private void showMessageGrantCancel(String message, DialogInterface.OnClickListener okListener) {
    new AlertDialog.Builder(MainActivity.this)
            .setMessage(message)
            .setPositiveButton("GRANT", okListener)
            .setNegativeButton("CANCEL", null)
            .create()
            .show();
}

Request and Handle Runtime permission

When your app receives PERMISSION_DENIED from checkSelfPermission(), you need to prompt the user for that permission.  To request the permissions Android provides a method called requestPermissions() to request array of permissions to be granted to the application and these permissions must be requested in your manifest. Calling these methods brings up a standard Android dialog, which you cannot customize. When the user responds to your app’s permission request, the system invokes your app’s onRequestPermissionsResult() method, passing it the user response.

Request runtime permission

  • If your app doesn’t already have the permission it needs, the app must call one of the requestPermissions() methods to request the appropriate permissions.
  • Your app passes the permissions it wants and an integer request code that you specify to identify this permission request.
  • This method functions asynchronously. It returns right away, and after the user responds to the prompt.
  • The system calls the app’s callback method onRequestPermissionsResult() with the results, passing the same request code that the app passed to requestPermissions().

The following code checks if the app has permission to access CAMERA. If it does not have permission it checks if it should show an explanation for needing the permission, and if no explanation is needed, it requests the permission:

// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
        Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {

        // Permission is not granted
        // Should we show an explanation?
        if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
        Manifest.permission.CAMERA)) {
        // Show an explanation to the user *asynchronously* -- don't block
        // this thread waiting for the user's response! After the user
        // sees the explanation, try again to request the permission.
        } else {
        // No explanation needed; request the permission
        ActivityCompat.requestPermissions(thisActivity,
        new String[]{ Manifest.permission.CAMERA},PERMISSION_REQUEST_CODE);

//PERMISSION_REQUEST_CODE is an
// app-defined int constant. The callback method gets the
// result of the request.
}
} else {
// Permission has already been granted
}

Handle the permissions request response

  • When the user responds to your app’s permission request, the system invokes your app’s onRequestPermissionsResult() method, passing it the user response. Your app has to override that method to find out whether the permission was granted. The callback is passed the same request code you passed to requestPermissions().

For example, if an app requests CAMERA and LOCATION access it might have the following callback method:

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case PERMISSION_REQUEST_CODE:

// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0) {

boolean AcceptedFineLocation = grantResults[0] == PackageManager.PERMISSION_GRANTED;
boolean AcceptedCamera = grantResults[1] == PackageManager.PERMISSION_GRANTED;

if (AcceptedFineLocation && AcceptedCamera)
Toast.makeText(this, "Permission Granted, Now you can access location data and camera.", Toast.LENGTH_LONG).show();

else {
Toast.makeText(this, "Permission Denied, You cannot access location data and camera.", Toast.LENGTH_LONG).show();
}
}
break;
}
}
  • If the user denies a permission request, your app should take appropriate action. For example, your app might show a dialog explaining why it could not perform the user’s requested action that needs that permission.
  • When the system asks the user to grant a permission, the user has the option of telling the system not to ask for that permission again. In that case, any time an app uses requestPermissions() to ask for that permission again, the system immediately denies the request.

Understand the workflow of runtime permission request

  • When the app asks for dangerous permission for the first time, then the system shows a standard non-customizable dialog that specifies the permission features.

Screenshot_1571749987     Screenshot_1571749993

  • If the user denies permission and the app tries to access the same permission again then it is a good programming practice to show an explanation to the user that why the app needs that permission. This is not a system dialog but showing explanation is the responsibility of the user.

Screenshot_1571750324

  • If a previously denied permission request again, then the system shows a standard dialog with an additional checkbox (Don’t ask again).
  • If the user selects the checkbox(Don’t ask again) then the system will never show that permission dialog again.

Screenshot_1571750895

  • If the user selects the checkbox(Don’t ask again)  and denies the permission for the second time then it is a good programming practice to lead the user to the permission page in app settings to grant the denied permissions.

Screenshot_1571750569        Screenshot_1571750591

So now let us start with creating a sample app.

Creating Android Project

1. Create a new project in Android Studio from File ⇒ New Project and fill the project details.

2. Open build.gradle (app level) and make sure that you have set minSdkVersion and targetSdkVersion as we have to support the permissions model in lower versions also. I am keeping minSdkVersion to 17 and targetSdkVersion to 28.

build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        ....

        minSdkVersion 17
        targetSdkVersion 28

        ....
        
    }

3. Though we will request the permissions at runtime, we should add it to the Manifest also. We will start with the CAMERA and ACCESS_FINE_LOCATION permissions. Add these permissions to your AndroidManifest.xml just before the application tag.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="trendlife.myapplication">
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.CAMERA"/>
    

    <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>

4. Open the layout file of main activity (activity_main.xml) and add the below xml. This layout contains two buttons ie. CHECK PERMISSION to check the permission and REQUEST PERMISSION to request the permissions.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/check_permission"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent"
        android:padding="8dp"
        android:text="Check Permission" />

    <Button
        android:id="@+id/request_permission"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:background="@color/colorAccent"
        android:padding="8dp"
        android:text="Request Permission" />


</LinearLayout>

The above layout generates a screen something like this.

Screenshot_1571751672
5. Now open MainActivity.java and write the below code on CHECK PERMISSION button click :
public void onClick(View v) {


    int id = v.getId();
    switch (id) {
        case R.id.check_permission:
            if (checkPermission()) {

                Toast.makeText(this, "Permission already granted.", Toast.LENGTH_LONG).show();

            } else {
                Toast.makeText(this, "Please request permission.", Toast.LENGTH_LONG).show();

            }
            break;
          }
       }


private boolean checkPermission() {
    int result1 = ContextCompat.checkSelfPermission(getApplicationContext(), ACCESS_FINE_LOCATION);
    int result2 = ContextCompat.checkSelfPermission(getApplicationContext(), CAMERA);

    return result1 == PackageManager.PERMISSION_GRANTED && result2 == PackageManager.PERMISSION_GRANTED;
}
6. In MainActivity.java write the below code on REQUEST PERMISSION button click :
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private static final int PERMISSION_REQUEST_CODE = 200;

@Override
public void onClick(View v) {


int id = v.getId();
switch (id) {

case R.id.request_permission:
if (!checkPermission()) {

requestPermission();

} else {
Toast.makeText(this, "Permission already granted.", Toast.LENGTH_LONG).show();


}
break;
}

}


private void requestPermission() {

ActivityCompat.requestPermissions(this, new String[]{ACCESS_FINE_LOCATION, CAMERA}, PERMISSION_REQUEST_CODE);

}


}
7. When the user responds to your app’s permission request, the system invokes your
app’s onRequestPermissionsResult() method corresponding to the request code, and passing it the user response.
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case PERMISSION_REQUEST_CODE:
            if (grantResults.length > 0) {

                boolean AcceptedFineLocation = grantResults[0] == PackageManager.PERMISSION_GRANTED;
                boolean AcceptedCamera = grantResults[1] == PackageManager.PERMISSION_GRANTED;

                if (AcceptedFineLocation && AcceptedCamera)
                    Toast.makeText(this, "Permission Granted, Now you can access location data and camera.", Toast.LENGTH_LONG).show();

                else {
                    Toast.makeText(this, "Permission Denied, You cannot access location data and camera.", Toast.LENGTH_LONG).show();


                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        if (shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION) || shouldShowRequestPermissionRationale(CAMERA)) {

                            showMessageGrantCancel("You need to allow access to both the permissions",
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                                requestPermissions(new String[]{ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, CAMERA}, PERMISSION_REQUEST_CODE);
                                            }
                                        }
                                    });
                            return;
                        } else {
                            openSettingsDialog();
                        }
                    }

                }
            }


            break;
    }

}
8. Below is the complete code of MainActivity.java.
MainActivity.java
package trendlife.myapplication;

import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.CAMERA;


public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final int PERMISSION_REQUEST_CODE = 200;


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


        Button check_permission = (Button) findViewById(R.id.check_permission);
        Button request_permission = (Button) findViewById(R.id.request_permission);
        check_permission.setOnClickListener(this);
        request_permission.setOnClickListener(this);


    }


    @Override
    public void onClick(View v) {


        int id = v.getId();
        switch (id) {
            case R.id.check_permission:
                if (checkPermission()) {

                    Toast.makeText(this, "Permission already granted.", Toast.LENGTH_LONG).show();

                } else {
                    Toast.makeText(this, "Please request permission.", Toast.LENGTH_LONG).show();

                }
                break;
            case R.id.request_permission:
                if (!checkPermission()) {

                    requestPermission();

                } else {
                    Toast.makeText(this, "Permission already granted.", Toast.LENGTH_LONG).show();


                }
                break;
        }

    }

    private boolean checkPermission() {
        int result1 = ContextCompat.checkSelfPermission(getApplicationContext(), ACCESS_FINE_LOCATION);
        int result2 = ContextCompat.checkSelfPermission(getApplicationContext(), CAMERA);

        return result1 == PackageManager.PERMISSION_GRANTED && result2 == PackageManager.PERMISSION_GRANTED;
    }


    private void requestPermission() {

        ActivityCompat.requestPermissions(this, new String[]{ACCESS_FINE_LOCATION, CAMERA}, PERMISSION_REQUEST_CODE);

    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_CODE:
                if (grantResults.length > 0) {

                    boolean AcceptedFineLocation = grantResults[0] == PackageManager.PERMISSION_GRANTED;
                    boolean AcceptedCamera = grantResults[1] == PackageManager.PERMISSION_GRANTED;

                    if (AcceptedFineLocation && AcceptedCamera)
                        Toast.makeText(this, "Permission Granted, Now you can access location data and camera.", Toast.LENGTH_LONG).show();

                    else {
                        Toast.makeText(this, "Permission Denied, You cannot access location data and camera.", Toast.LENGTH_LONG).show();


                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                            if (shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION) || shouldShowRequestPermissionRationale(CAMERA)) {

                                showMessageGrantCancel("You need to allow access to both the permissions",
                                        new DialogInterface.OnClickListener() {
                                            @Override
                                            public void onClick(DialogInterface dialog, int which) {
                                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                                    requestPermissions(new String[]{ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, CAMERA}, PERMISSION_REQUEST_CODE);
                                                }
                                            }
                                        });
                                return;
                            } else {
                                openSettingsDialog();
                            }
                        }

                    }
                }


                break;
        }

    }


    private void showMessageGrantCancel(String message, DialogInterface.OnClickListener okListener) {
        new AlertDialog.Builder(MainActivity.this)
                .setMessage(message)
                .setPositiveButton("GRANT", okListener)
                .setNegativeButton("CANCEL", null)
                .create()
                .show();
    }

    private void openSettingsDialog() {

        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        builder.setTitle("Required Permissions");
        builder.setMessage("This app require permission to use awesome feature. Grant them in app settings.");
        builder.setPositiveButton("Take Me To SETTINGS", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                Uri uri = Uri.fromParts("package", getPackageName(), null);
                intent.setData(uri);
                startActivityForResult(intent, 101);
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
            }
        });
        builder.show();

    }
}


I hope this article gave you very good overview of Marshmallow permission model. Feel free to ask any queries / doubts in comments section below.

Leave a Reply