godot/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java

/**************************************************************************/
/*  PermissionsUtil.java                                                  */
/**************************************************************************/
/*                         This file is part of:                          */
/*                             GODOT ENGINE                               */
/*                        https://godotengine.org                         */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
/*                                                                        */
/* Permission is hereby granted, free of charge, to any person obtaining  */
/* a copy of this software and associated documentation files (the        */
/* "Software"), to deal in the Software without restriction, including    */
/* without limitation the rights to use, copy, modify, merge, publish,    */
/* distribute, sublicense, and/or sell copies of the Software, and to     */
/* permit persons to whom the Software is furnished to do so, subject to  */
/* the following conditions:                                              */
/*                                                                        */
/* The above copyright notice and this permission notice shall be         */
/* included in all copies or substantial portions of the Software.        */
/*                                                                        */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
/**************************************************************************/

package org.godotengine.godot.utils;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * This class includes utility functions for Android permissions related operations.
 */
public final class PermissionsUtil {
	private static final String TAG = PermissionsUtil.class.getSimpleName();

	public static final int REQUEST_RECORD_AUDIO_PERMISSION = 1;
	public static final int REQUEST_CAMERA_PERMISSION = 2;
	public static final int REQUEST_VIBRATE_PERMISSION = 3;
	public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
	public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002;
	public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;

	private PermissionsUtil() {
	}

	/**
	 * Request a list of dangerous permissions. The requested permissions must be included in the app's AndroidManifest
	 * @param permissions list of the permissions to request.
	 * @param activity the caller activity for this method.
	 * @return true/false. "true" if permissions are already granted, "false" if a permissions request was dispatched.
	 */
	public static boolean requestPermissions(Activity activity, List<String> permissions) {
		if (activity == null) {
			return false;
		}

		if (permissions == null || permissions.isEmpty()) {
			return true;
		}

		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
			// Not necessary, asked on install already
			return true;
		}

		Set<String> requestedPermissions = new HashSet<>();
		for (String permission : permissions) {
			try {
				if (permission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
						Log.d(TAG, "Requesting permission " + permission);
						try {
							Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
							intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
							activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
						} catch (Exception ignored) {
							Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
							activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
						}
					}
				} else {
					PermissionInfo permissionInfo = getPermissionInfo(activity, permission);
					int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
					if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
						Log.d(TAG, "Requesting permission " + permission);
						requestedPermissions.add(permission);
					}
				}
			} catch (PackageManager.NameNotFoundException e) {
				// Skip this permission and continue.
				Log.w(TAG, "Unable to identify permission " + permission, e);
			}
		}

		if (requestedPermissions.isEmpty()) {
			// If list is empty, all of dangerous permissions were granted.
			return true;
		}

		activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
		return false;
	}

	/**
	 * Request a dangerous permission. The requested permission must be included in the app's AndroidManifest
	 * @param permissionName the name of the permission to request.
	 * @param activity the caller activity for this method.
	 * @return true/false. "true" if permission is already granted, "false" if a permission request was dispatched.
	 */
	public static boolean requestPermission(String permissionName, Activity activity) {
		if (activity == null || TextUtils.isEmpty(permissionName)) {
			return false;
		}

		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
			// Not necessary, asked on install already
			return true;
		}

		switch (permissionName) {
			case "RECORD_AUDIO":
			case Manifest.permission.RECORD_AUDIO:
				if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
					activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION);
					return false;
				}
				return true;

			case "CAMERA":
			case Manifest.permission.CAMERA:
				if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
					activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION);
					return false;
				}
				return true;

			case "VIBRATE":
			case Manifest.permission.VIBRATE:
				if (ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) {
					activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION);
					return false;
				}
				return true;

			default:
				// Check if the given permission is a dangerous permission
				try {
					PermissionInfo permissionInfo = getPermissionInfo(activity, permissionName);
					int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
					if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) {
						activity.requestPermissions(new String[] { permissionName }, REQUEST_SINGLE_PERMISSION_REQ_CODE);
						return false;
					}
				} catch (PackageManager.NameNotFoundException e) {
					// Unknown permission - return false as it can't be granted.
					Log.w(TAG, "Unable to identify permission " + permissionName, e);
					return false;
				}
				return true;
		}
	}

	/**
	 * Request dangerous permissions which are defined in the Android manifest file from the user.
	 * @param activity the caller activity for this method.
	 * @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched.
	 */
	public static boolean requestManifestPermissions(Activity activity) {
		return requestManifestPermissions(activity, null);
	}

	/**
	 * Request dangerous permissions which are defined in the Android manifest file from the user.
	 * @param activity the caller activity for this method.
	 * @param excludes Set of permissions to exclude from the request
	 * @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched.
	 */
	public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) {
		if (activity == null) {
			return false;
		}

		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
			return true;
		}

		List<String> manifestPermissions;
		try {
			manifestPermissions = getManifestPermissions(activity);
		} catch (PackageManager.NameNotFoundException e) {
			e.printStackTrace();
			return false;
		}

		if (manifestPermissions.isEmpty()) {
			return true;
		}

		if (excludes != null && !excludes.isEmpty()) {
			for (String excludedPermission : excludes) {
				manifestPermissions.remove(excludedPermission);
			}
		}

		return requestPermissions(activity, manifestPermissions);
	}

	/**
	 * With this function you can get the list of dangerous permissions that have been granted to the Android application.
	 * @param context the caller context for this method.
	 * @return granted permissions list
	 */
	public static String[] getGrantedPermissions(Context context) {
		List<String> manifestPermissions;
		try {
			manifestPermissions = getManifestPermissions(context);
		} catch (PackageManager.NameNotFoundException e) {
			e.printStackTrace();
			return new String[0];
		}
		if (manifestPermissions.isEmpty()) {
			return new String[0];
		}

		List<String> grantedPermissions = new ArrayList<>();
		for (String manifestPermission : manifestPermissions) {
			try {
				if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
						grantedPermissions.add(manifestPermission);
					}
				} else {
					PermissionInfo permissionInfo = getPermissionInfo(context, manifestPermission);
					int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
					if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
						grantedPermissions.add(manifestPermission);
					}
				}
			} catch (PackageManager.NameNotFoundException e) {
				// Skip this permission and continue.
				Log.w(TAG, "Unable to identify permission " + manifestPermission, e);
			}
		}

		return grantedPermissions.toArray(new String[0]);
	}

	/**
	 * Check if the given permission is in the AndroidManifest.xml file.
	 * @param context the caller context for this method.
	 * @param permission the permission to look for in the manifest file.
	 * @return "true" if the permission is in the manifest file of the activity, "false" otherwise.
	 */
	public static boolean hasManifestPermission(Context context, String permission) {
		try {
			for (String p : getManifestPermissions(context)) {
				if (permission.equals(p)) {
					return true;
				}
			}
		} catch (PackageManager.NameNotFoundException ignored) {
		}

		return false;
	}

	/**
	 * Returns the permissions defined in the AndroidManifest.xml file.
	 * @param context the caller context for this method.
	 * @return mutable copy of manifest permissions list
	 * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
	 */
	public static ArrayList<String> getManifestPermissions(Context context) throws PackageManager.NameNotFoundException {
		PackageManager packageManager = context.getPackageManager();
		PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
		if (packageInfo.requestedPermissions == null) {
			return new ArrayList<>();
		}
		return new ArrayList<>(Arrays.asList(packageInfo.requestedPermissions));
	}

	/**
	 * Returns the information of the desired permission.
	 * @param context the caller context for this method.
	 * @param permission the name of the permission.
	 * @return permission info object
	 * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
	 */
	private static PermissionInfo getPermissionInfo(Context context, String permission) throws PackageManager.NameNotFoundException {
		PackageManager packageManager = context.getPackageManager();
		return packageManager.getPermissionInfo(permission, 0);
	}
}