Often when installing an application on Android, we had to see that it asks for some unthinkable number of permissions. For example:
Well, if you install the application from a well-known developer who you can trust. But it is very suspicious if you install a new music player, and for work it is required, for example, to receive your location. Or, especially, a flashlight that requires access to SMS and calls.
')
Some developers, in order to reduce mistrust, add to the description of the application on Google Play why they need this or that permission.
By the sixth version of Android, the situation has changed. Now permissions must be requested in the process. How this new opportunity to use and some of its pitfalls will be discussed below.
general information
Just as it happens in iOS, when prompted, a system dialog will appear asking for permission.
The difference is that after pressing the “Deny” button, the permission will not be completely prohibited for the application, as it happens with Apple. You can request it again, but in this case the option “Never ask again” will appear, after selecting which “Deny” works like “Don't allow” in iOS.
Permissions are divided into two types (there are others, but they do not interest us):
- normal (normal);
- dangerous (dangerous).
Normal permissions will be obtained by the application during installation, no confirmation from the user is required (a little controversial point, in my opinion, it would be worth notifying the user of the required permissions). In the future, to withdraw them from the application will be impossible. Dangerous must be requested during the operation of the application and can be withdrawn at any time. The list of dangerous and not very permissions can be found
here .
You can see that access to the Internet is not considered dangerous. Anyone who uses advertising in their programs can breathe a sigh of relief: you can’t turn it off just by selecting permission (you can still just turn off the Internet, but the fact remains).
In order to revoke the permission that was previously issued (or grant it if you selected “Never ask again”), go to the application settings (Settings-> Apps -> * AppName *) in the Permissions section and click on the appropriate switches. In this menu, you can also view all the permissions of this program by selecting “All permissions” from the context menu. You can also see which applications have been given a specific permission (and, accordingly, grant or select it). To do this, in the settings in the Apps section, click on the menu with the gear icon and select App permissions in the opened section. Next, selecting the desired resolution, you can see all the applications that need it.
User interaction
Let's see how to make user interaction. We start directly with a request for permission. With normal permissions, everything is clear, this is the application installer’s business, it doesn’t interest us, and how we request dangerous permissions depends on two things.
The first one is the importance of this permission for your application, it depends on when you need to make a request. If the function is critical and without it the program will not make sense, then feel free to ask permission right at the start of the application. For example, if you are developing an application for the exchange of sms, then without the appropriate permissions you can’t do anything with it, it loses all meaning. And if the user refused, then we do not skip it further, but we give the opportunity to once again trigger the request dialog and give instructions on what to do.
If the resolution requires some kind of secondary function, then you do not need to ask about it immediately. Do this only when the user wants to take advantage of this opportunity.
The second point is how clear it will be to the person what this permission is for. Why does the SMS application have access to the calendar? Probably for some cool function that will facilitate the transfer of dates from messages to the calendar and the like. But only you know about it, so you first need to explain the reason for the request and show what possibilities will give access to this permission. This applies to both primary and secondary permissions.
Once again briefly:
- important permissions are requested at startup, secondary permissions when the corresponding function is first used;
- If you understand why permission is necessary, we provide an explanation.
In the case when you still refused, an explanation of the reason for the next time is mandatory. And if, in addition to the refusal, the user asked you never to request this permission using the “Never ask again” option, but tries to use the appropriate function of the application, ask him to go to your program settings and manually enable the necessary permissions. This is especially important if the resolution is critical for the operation of the program.
Code
The logical question will be: what happens if you run your unsupported permissions under runtime permissions application on Android Marshmallow? The answer depends on whether you dare to change targetSdk to version 23 (compileSdk and buildTools are not interested in this case). If yes, then I have not the best news for you: it is very likely that you will get a SecurityException. This will not necessarily be the case; perhaps somewhere you will get null instead of the requested information, but the probability is far from zero. If you use targetSdk version 22 and below, then all permissions will be issued to the application as before, including dangerous ones. This does not negate the fact that the user may revoke any of them after installation. At the same time, he will receive a warning that the application is not adapted to runtime permissions and may not work correctly. How incorrect it will work is completely up to you, that is, if you checked the returned values ​​to null or were ready to zero instead of the sane value, then nothing terrible will happen: the application simply will not fully function (which looks still better than the fall from due to NullPointerException).
But even if you are doing well with checks and there is no opportunity to engage in the introduction of new features, you should double-check whether everything works correctly, because sometimes you can get null not where you expect it. So, for example, when using Environment.getExternalStorageDirectory () without permission from the Storage group, we get File, but list () will return us the cherished null. The documentation describes this outcome, but for the situation when File is not a directory. So check in any case will not be superfluous.
It is possible to add permission only for Android M and above. To do this, you need to use the new <uses-sdk-23 /> tag (previously called <uses-sdk-m />) in the manifest. Its syntax is similar to the usual <uses-permission />. This can be useful if you want to add an ability to an existing application that requires additional permission. As you remember, if the new version of the program requires additional rights, the user must manually confirm its update. This can be avoided if the new function is not very important by disabling it for earlier versions of the system, using the previously described tag. In this case, this permission will be absent altogether.
In the process of debugging often have to enable / disable permissions. Going to this every time in the application settings is not very convenient. Fortunately, this can be done using adb:
adb shell pm grant <app package> <permission name> adb shell pm revoke <app package> <permission name>
And a few more useful commands, the meaning of which is clear from the title:
adb shell pm reset-permissions adb shell pm list permission-groups adb shell pm list permissions
Let us proceed to the immediate implementation (we will not forget to upgrade compileSdkVersion and targetSdkVersion to version 23 beforehand).
Until the moment when Marshmallow becomes the minimum version of android for your applications, it is still far, so you need to take care of backward compatibility. Of course, you can check the version of sdk, but why, if everything is implemented for us in the support library v4 (ActivityCompat) and v13 (FragmentCompat). If you still need the original methods, then find them is not difficult.
All examples use ActivityCompat, as they were made for the activity. For fragment you need to use FragmentCompat. If for some reason you do not use activity and fragment from the support libraries, then you need to implement the ActivityCompat.OnRequestPermissionsResultCallback or FragmentCompat.OnRequestPermissionsResultCallback interface, respectively.
Every time we want to use a method that requires dangerous permission, it is necessary to check whether we have it. To do this, we use the ContextCompat.checkSelfPermission (Context context, String permission) method, which returns us one of the int values: PackageManager.PERMISSION_GRANTED in case there is permission or PackageManager.PERMISSION_DENIED if it is not present. The resolution name is one of the constants of the class Manifest.permission.
@Nullable public File[] getExternalStorageFiles() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { final File externalStorage = Environment.getExternalStorageDirectory(); if (externalStorage != null) { return externalStorage.listFiles(); } } return null; }
Further, if there is permission, we perform the action we need, and if not, then we need to request it. At the same time, you can request multiple permissions (the user will be shown a request for each of them in turn), if necessary.
It is worth mentioning that if the permissions are in the same permission group, then you only need to request one of them, since all the other elements of this group will also be available. But this is not necessary. Because in the future, the composition of groups may change, so when requesting permits, it is not necessary to make assumptions as to whether they are in the same group or not.
UPD as if in confirmation of the previous paragraph, starting with Android 8.0, permissions from one permission group are not issued immediately - each permission must be requested separately, but all permissions from one group will be issued automatically, without user participation, at their first request.
UPD2 the same behavior was noticed on Android 7.0 - if part of the permissions from the group were issued (I can not say with certainty whether it matters which ones), then the rest will be issued on request immediately without showing a dialog. This can cause problems if your application explains to the user why she needs this or that permission even before his request. In real life, this rarely can occur (only when using adb commands), but you should take this into account when debugging an application.
The request uses the ActivityCompat.requestPermissions method (Activity activity, String [] permissions, int requestCode). The permissions array accordingly contains the names of the permissions you want to request. This shows that you can simultaneously request multiple permissions. requestCode is a value by which in the future it will be possible to determine which request for permission you received an answer just as we get the result from the activity using startActivityForResult. By the way, if you look at the requestPermission code, you will find that this is just a special version of startActivityForResult.
public void requestMultiplePermissions() { ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_SMS }, PERMISSION_REQUEST_CODE); }
As you can see, you can directly request permissions only from an Activity or Fragment. If permission is required by the service, you will have to start an Activity, from which you can already make a request. The best thing to do is to show a notification containing information about the missing resolution with a button to launch this Activity itself.
The result of the permission request should be processed in onRequestPermissionsResult (int requestCode, @NonNull String [] permissions, @NonNull int [] grantResults). The requestCode and permissions parameters contain the data you passed when requesting permissions. The main data here is carried by an array of grantResults, which contains information about whether or not permissions are obtained. Each i-th permissions element corresponds to the i-th element from grantResults. Their possible values ​​are similar to the checkSelfPermission result.
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_CODE && grantResults.length == 2) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { showExtDirFilesCount(); } if (grantResults[1] == PackageManager.PERMISSION_GRANTED) { showUnreadSmsCount(); } } super.onRequestPermissionsResult(requestCode, permissions, grantResults); }
The size of the grantResults array is checked to ensure that the permission request has not been interrupted (in this case, the permissions and grantResults will not contain elements). Such a situation should not be viewed as a prohibition of permission, but as a cancellation of a request for it.
If you have previously requested permission, but the user declined to provide it, you must explain the reason for the request. This need not be done if the reason for which you are requesting permission is absolutely clear. If there is a possibility that the question “Why does the application need it?” Will arise, then this is highly desirable to explain. To find out if an explanation needs to be shown, there is the method shouldShowRequestPermissionRationale (@NonNull Activity activity, @NonNull String permission), which is returned by the boolean. The very same explanation can be implemented, for example, with the help of Snackbar with an action button, upon clicking on which the permission is requested, or in a dialog box if the permission is critical.
public void requestPermissionWithRationale() { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { final String message = "Storage permission is needed to show files count"; Snackbar.make(view_, message, Snackbar.LENGTH_LONG) .setAction("GRANT", new View.OnClickListener() { @Override public void onClick(View v) { requestReadExtStorage(); } }) .show(); } else { requestReadExtStorage(); } }
Never ask again
One of the problems may be the option “Never ask again”, which appears when you re-request permission, after the user has already refused earlier. As the name implies, when you select it, the request dialog will no longer appear. shouldShowRequestPermissionRationale will give out false, and on onquestPermissionsResult will get the result PackageManager.PERMISSION_DENIED. And we will get permission only if we enable it directly through the application settings in the Permissions section.
What can be done with this? First of all, of course, inform the user that there are no necessary rights to perform the action. Further possible action may be a suggestion to go to the settings and grant this permission manually. Not the best option, but better than nothing. This can be implemented again using the Snackbar with an action button.
Go directly to the page with permissions will not work, so the best thing you can do is open the settings of your application. After that you can, for example, show Toast with the information that needs to be done.
public void showNoStoragePermissionSnackbar() { Snackbar.make(view_, "Storage permission isn't granted" , Snackbar.LENGTH_LONG) .setAction("SETTINGS", new View.OnClickListener() { @Override public void onClick(View v) { openApplicationSettings(); Toast.makeText(getApplicationContext(), "Open Permissions and grant the Storage permission", Toast.LENGTH_SHORT) .show(); } }) .show(); } public void openApplicationSettings() { Intent appSettingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + getPackageName())); startActivityForResult(appSettingsIntent, PERMISSION_REQUEST_CODE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PERMISSION_REQUEST_CODE) { showExtDirFilesCount(); return; } super.onActivityResult(requestCode, resultCode, data); }
The example uses startActivityForResult and onActivityResult to determine that the user returned from the settings activity back to the application and try an action that could not be done without the required permission. In the showExtDirFilesCount method, you need to check again whether there is permission to ensure that the user issued it after all.
There may be a situation that does not particularly hinder if you use Snackbar to show rationale, but spoils the UX, if you decide to use dialogs (we do not touch on the reasons for this decision). Namely, the double appearance of rationale, before the request for permission and after it. How can this happen? We have only two methods by which we can judge the state of resolution. The problem is that before requesting permission, the situation when we have never requested this permission, and the situation when the user previously chose “Never ask again”, is exactly the same in terms of values. Namely, checkSeflPermission returns us PERMISSION_DENIED, a shouldShowRequestPermissionRationale - false. So, we’ll show a dialog for opening settings in onRequestPermissionsResult, where the value of shouldShowRequestPermissionRationale will be exactly different for these two situations. All perfectly? Not really. In this callback, there is no way to determine whether a rationale was shown or not. Therefore, if you show the reason for the request, and then the user asks him not to ask about this permission anymore, after clicking on the DENY button, he will receive another rationale dialog inviting him to the program settings. Good programs do not behave this way.
What to do in this situation? There are a couple of not very nice solutions on the network: one of them is to save information about whether you have permission or not in SharedPreferences, and the other to store a flag about whether a rationale was shown inside a class or not. The first decision is not good because while the application is not working, the user can change the permissions settings and the information in the preferences will be irrelevant. The second method is not very beautiful.
A good option (in my opinion) is to have two requestCode for each request, one for use in rationale and the other in other cases. This method is also not perfect and not particularly beautiful, but it helps to adhere to existing methods without contributing anything new.
Intent
There is another important recommendation when using runtime permissions. Do not use them. More precisely, use it, but only when the functionality that you are going to implement with their help, has not been done by someone before you.
As the most illustrative example, the camera is most often remembered. Use the standard camera application (or other applications that can do this) if you only need to take a photo without some special logic. Intensions will help you with this (
more ).
Thus, you can get rid of some dangerous permissions and simplify work with the application.
Conclusion
. , , (, , , ). , , . , , , , ( , ). , , Google , , .