Taking Photos Not So Simply: How I Got Bitten By ACTION_IMAGE_CAPTURE

I've worked on a number of apps that are taking pictures for different use cases, and since it has always been a secondary feature for those apps, I've relied on using Intents, the approach described in Taking Photos Simply. Intents are powerful, they can save you lots of man-hours by providing the possibility to reuse the functionality of other apps inside the system, rather than reinventing the wheel. Additionally, as explained in Permissions Best Practices, Intents can help you keep the number of permissions that your app uses low, making it more trustworthy. However, today I've stumbled upon a very interesting edge case related to MediaStore.ACTION_IMAGE_CAPTURE, which really made me wonder if everything is that easy.

I'm working on a library that gets integrated by two apps. Internally, the library takes pictures by firing an Intent with MediaStore.ACTION_IMAGE_CAPTURE. Testing discovered that whenever the image capturing is invoked in one of the apps, the app crashes with roughly the following error message:

ActivityManager: Permission Denial: starting Intent  
{ act=android.media.action.IMAGE_CAPTURE flg=0x3000000 pkg=com.google.android.GoogleCamera 
cmp=com.google.android.GoogleCamera/com.android.camera.CaptureActivity  
(has clip) (has extras) } from null (pid=-1, uid=10098) 
with revoked permission android.permission.CAMERA  

The with revoked permission android.permission.CAMERA looked troubling: indeed, the app requires android.permission.CAMERA for a different feature and hasn't been requested at runtime yet, but what does it have to do with the Intent? I started googling and found an issue report, describing this exact problem. Interestingly enough, the resolution is "WorkingAsIntended", and indeed the documentation for ACTION_IMAGE_CAPTURE says:

Note: if you app targets M and above and declares as using the CAMERA permission which is not granted, then atempting to use this action will result in a SecurityException.

This is how this behavior is explained by a Google engineer, commenting on the ticket:

This is intended behavior to avoid user frustration where they revoked the camera permission from an app and the app still being able to take photos via the intent. Users are not aware that the photo taken after the permission revocation happens via different mechanism and would question the correctness of the permission model. This applies to MediaStore.ACTION_IMAGE_CAPTURE, MediaStore.ACTION_VIDEO_CAPTURE, and Intent.ACTION_CALL the docs for which document the behavior change for apps targeting M.

That was a very interesting finding. The rationale makes sense to me, however, I doubt that these are the only three scenarios that may confuse the user while facing a feature implemented using Intents. I guess most users don't notice the switch and expect to not see the camera being used if they haven't explicitly allowed it. This questions the whole idea of using Intents though... Another thing to note is that "revoked" and "was never requested" are probably two different states, which are however treated in the same way by the system. On Android 6, the user might not even know that the app will ask her to allow it to use the Camera at some point, therefore the use case of taking pictures via Intent will feel the same, independent of whether the permission has been declared in the Manifest or not, therefore the behavior of the system seems inconsistent to me. What's also disappointing is that articles that I read about runtime permissions, official and unofficial, present Intents as a "free alternative", and neither mentions these edge cases. I understand though, that most apps will either use Intents, or declare android.permission.CAMERA and use the Camera directly, but the library use case is still a valid one.

Anyway, I had to deal with it, and the most straightforward solution I could think of was to add the Camera permission into both apps - not ideal, since the other app doesn't in fact need it. Luckily, I stumbled upon a nice solution on StackOverflow, suggesting to check if android.permission.CAMERA has been declared in the Manifest and then request it at runtime prior to firing the Intent. The code snippet looks like this:

private static boolean hasPermissionInManifest(@NonNull Context context,  
        @NonNull String permissionName) {
    String packageName = context.getPackageName();
    try {
        PackageInfo packageInfo = context.getPackageManager()
                .getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
        String[] declaredPermissions = packageInfo.requestedPermissions;
        if (declaredPermissions != null) {
            for (String p : declaredPermissions) {
                if (p.equals(permissionName)) {
                    return true;
                }
            }
        }
    } catch (PackageManager.NameNotFoundException e) {
        // ignored
    }
    return false;
}

Now, whenever I'd need to check if I have all necessary permissions granted, I could use the following code:

public static boolean checkForPhotoPermissionsIfNeeded(@NonNull Fragment fragment,  
        int requestCode) {
    boolean cameraPermissionDeclared = hasPermissionInManifest(fragment.getContext(), 
        Manifest.permission.CAMERA);
    int storagePermissionStatus =
            ContextCompat.checkSelfPermission(fragment.getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
    int cameraPermissionStatus = !cameraPermissionDeclared ? 
            PackageManager.PERMISSION_GRANTED :
            ContextCompat.checkSelfPermission(fragment.getContext(), Manifest.permission.CAMERA);
    if (storagePermissionStatus == PackageManager.PERMISSION_GRANTED &&
            cameraPermissionStatus == PackageManager.PERMISSION_GRANTED) {
        return true;
    }
    String[] permissionsToRequest = cameraPermissionDeclared ?
            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, 
                    Manifest.permission.CAMERA} :
            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
    fragment.requestPermissions(permissionsToRequest, requestCode);
    return false;
}

This definitely adds a number of test cases, but solves the problem and works fine in both apps.

Conclusion

Be careful when using Intents, as sometimes they don't come for free, as you'd expect. Pay attention to the documentation of Intent actions you use, especially in the context of Android M. Read this amazing article by CommonsWare, which states that using ACTION_IMAGE_CAPTURE might actually be a bad idea. Test your code and love Android!
Cheers!