Changed|Fixed: Add java docs to PermissionUtils and fix permission checking

ContextCompat.checkSelfPermission() may return true for permissions not even requested so it now checked if permissions are even requested in app manifest before checking if they are granted and before asking for permission to be granted.

Also some general improvements in code quality, including using ActivityUtils to request non-standard permissions and added support for AppCompatActivity instances to request permissions in addition to Activity instances.
This commit is contained in:
agnostic-apollo
2021-10-21 23:10:00 +05:00
parent 50a97b1977
commit 32dd7eab03
2 changed files with 205 additions and 33 deletions

View File

@@ -1,21 +1,32 @@
package com.termux.shared.packages;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.google.common.base.Joiner;
import com.termux.shared.R;
import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Error;
import com.termux.shared.models.errors.FunctionErrno;
import com.termux.shared.view.ActivityUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class PermissionUtils {
@@ -27,66 +38,206 @@ public class PermissionUtils {
private static final String LOG_TAG = "PermissionUtils";
public static boolean checkPermission(Context context, String permission) {
if (permission == null) return false;
/**
* Check if app has been granted the required permission.
*
* @param context The context for operations.
* @param permission The {@link String} name for permission to check.
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
*/
public static boolean checkPermission(@NonNull Context context, @NonNull String permission) {
return checkPermissions(context, new String[]{permission});
}
public static boolean checkPermissions(Context context, String[] permissions) {
if (permissions == null) return false;
/**
* Check if app has been granted the required permissions.
*
* @param context The context for operations.
* @param permissions The {@link String[]} names for permissions to check.
* @return Returns {@code true} if permissions are granted, otherwise {@code false}.
*/
public static boolean checkPermissions(@NonNull Context context, @NonNull String[] permissions) {
// checkSelfPermission may return true for permissions not even requested
List<String> permissionsNotRequested = getPermissionsNotRequested(context, permissions);
if (permissionsNotRequested.size() > 0) {
Logger.logError(LOG_TAG,
context.getString(R.string.error_attempted_to_check_for_permissions_not_requested,
Joiner.on(", ").join(permissionsNotRequested)));
return false;
}
int result;
for (String p:permissions) {
result = ContextCompat.checkSelfPermission(context,p);
for (String permission : permissions) {
result = ContextCompat.checkSelfPermission(context, permission);
if (result != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
public static void requestPermission(Activity activity, String permission, int requestCode) {
if (permission == null) return;
requestPermissions(activity, new String[]{permission}, requestCode);
/**
* Request user to grant required permissions to the app.
*
* @param context The context for operations. It must be an instance of {@link Activity} or
* {@link AppCompatActivity}.
* @param permission The {@link String} name for permission to request.
* @param requestCode The request code to use while asking for permission. It must be `>=0` or
* will fail silently and will log an exception.
* @return Returns {@code true} if requesting the permission was successful, otherwise {@code false}.
*/
public static boolean requestPermission(@NonNull Context context, @NonNull String permission,
int requestCode) {
return requestPermissions(context, new String[]{permission}, requestCode);
}
public static void requestPermissions(Activity activity, String[] permissions, int requestCode) {
if (activity == null || permissions == null) return;
/**
* Request user to grant required permissions to the app.
*
* On sdk 30 (android 11), Activity.onRequestPermissionsResult() will pass
* {@link PackageManager#PERMISSION_DENIED} (-1) without asking the user for the permission
* if user previously denied the permission prompt. On sdk 29 (android 10),
* Activity.onRequestPermissionsResult() will pass {@link PackageManager#PERMISSION_DENIED} (-1)
* without asking the user for the permission if user previously selected "Deny & don't ask again"
* option in prompt. The user will have to manually enable permission in app info in Android
* settings. If user grants and then denies in settings, then next time prompt will shown.
*
* @param context The context for operations. It must be an instance of {@link Activity} or
* {@link AppCompatActivity}.
* @param permissions The {@link String[]} names for permissions to request.
* @param requestCode The request code to use while asking for permissions. It must be `>=0` or
* will fail silently and will log an exception.
* @return Returns {@code true} if requesting the permissions was successful, otherwise {@code false}.
*/
public static boolean requestPermissions(@NonNull Context context, @NonNull String[] permissions,
int requestCode) {
List<String> permissionsNotRequested = getPermissionsNotRequested(context, permissions);
if (permissionsNotRequested.size() > 0) {
Logger.logErrorAndShowToast(context, LOG_TAG,
context.getString(R.string.error_attempted_to_ask_for_permissions_not_requested,
Joiner.on(", ").join(permissionsNotRequested)));
return false;
}
int result;
Logger.showToast(activity, activity.getString(R.string.message_sudo_please_grant_permissions), true);
try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
for (String permission:permissions) {
result = ContextCompat.checkSelfPermission(activity, permission);
for (String permission : permissions) {
int result = ContextCompat.checkSelfPermission(context, permission);
// If at least one permission not granted
if (result != PackageManager.PERMISSION_GRANTED) {
Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
Logger.logInfo(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
try {
activity.requestPermissions(new String[]{permission}, requestCode);
if (context instanceof AppCompatActivity)
((AppCompatActivity) context).requestPermissions(new String[]{permission}, requestCode);
else if (context instanceof Activity)
((Activity) context).requestPermissions(new String[]{permission}, requestCode);
else {
Error.logErrorAndShowToast(context, LOG_TAG,
FunctionErrno.ERRNO_PARAMETER_NOT_INSTANCE_OF.getError("context", "requestPermissions", "Activity or AppCompatActivity"));
return false;
}
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to request permissions with request code " + requestCode + ": " + Arrays.toString(permissions), e);
String errmsg = context.getString(R.string.error_failed_to_request_permissions, requestCode, Arrays.toString(permissions));
Logger.logStackTraceWithMessage(LOG_TAG, errmsg, e);
Logger.showToast(context, errmsg + "\n" + e.getMessage(), true);
return false;
}
}
}
return true;
}
public static boolean checkDisplayOverOtherAppsPermission(Context context) {
/**
* Check if app has requested the required permission in the manifest.
*
* @param context The context for operations.
* @param permission The {@link String} name for permission to check.
* @return Returns {@code true} if permission has been requested, otherwise {@code false}.
*/
public static boolean isPermissionRequested(@NonNull Context context, @NonNull String permission) {
return getPermissionsNotRequested(context, new String[]{permission}).size() == 0;
}
/**
* Check if app has requested the required permissions or not in the manifest.
*
* @param context The context for operations.
* @param permissions The {@link String[]} names for permissions to check.
* @return Returns {@link List<String>} of permissions that have not been requested. It will have
* size 0 if all permissions have been requested.
*/
@NonNull
public static List<String> getPermissionsNotRequested(@NonNull Context context, @NonNull String[] permissions) {
List<String> permissionsNotRequested = new ArrayList<>();
Collections.addAll(permissionsNotRequested, permissions);
PackageInfo packageInfo = PackageUtils.getPackageInfoForPackage(context, PackageManager.GET_PERMISSIONS);
if (packageInfo == null) {
return permissionsNotRequested;
}
// If no permissions are requested, then nothing to check
if (packageInfo.requestedPermissions == null || packageInfo.requestedPermissions.length == 0)
return permissionsNotRequested;
List<String> requestedPermissionsList = Arrays.asList(packageInfo.requestedPermissions);
for (String permission : permissions) {
if (requestedPermissionsList.contains(permission)) {
permissionsNotRequested.remove(permission);
}
}
return permissionsNotRequested;
}
/**
* Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW} permission has been granted.
*
* @param context The context for operations.
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
*/
public static boolean checkDisplayOverOtherAppsPermission(@NonNull Context context) {
return Settings.canDrawOverlays(context);
}
public static void requestDisplayOverOtherAppsPermission(Activity context, int requestCode) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
context.startActivityForResult(intent, requestCode);
/**
* Request user to grant {@link Manifest.permission#SYSTEM_ALERT_WINDOW} permission to the app.
*
* @param context The context for operations. It must be an instance of {@link Activity} or
* {@link AppCompatActivity}.
* @param requestCode The request code to use while asking for permission. It must be `>=0` or
* will fail silently and will log an exception.
* @return Returns {@code true} if requesting the permission was successful, otherwise {@code false}.
*/
public static boolean requestDisplayOverOtherAppsPermission(@NonNull Context context, int requestCode) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + context.getPackageName()));
return ActivityUtils.startActivityForResult(context, requestCode, intent);
}
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(Context context, boolean logResults) {
/**
* Check if running on sdk 29 (android 10) or higher and {@link Manifest.permission#SYSTEM_ALERT_WINDOW}
* permission has been granted or not.
*
* @param context The context for operations.
* @param logResults If it should be logged that permission has been granted or not.
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
*/
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(@NonNull Context context,
boolean logResults) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true;
if (!PermissionUtils.checkDisplayOverOtherAppsPermission(context)) {
if (!checkDisplayOverOtherAppsPermission(context)) {
if (logResults)
Logger.logWarn(LOG_TAG, context.getPackageName() + " does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission");
return false;
@@ -99,16 +250,35 @@ public class PermissionUtils {
public static boolean checkIfBatteryOptimizationsDisabled(Context context) {
/**
* Check if {@link Manifest.permission#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS} permission has been
* granted.
*
* @param context The context for operations.
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
*/
public static boolean checkIfBatteryOptimizationsDisabled(@NonNull Context context) {
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
}
/**
* Request user to grant {@link Manifest.permission#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS}
* permission to the app.
*
* @param context The context for operations. It must be an instance of {@link Activity} or
* {@link AppCompatActivity}.
* @param requestCode The request code to use while asking for permission. It must be `>=0` or
* will fail silently and will log an exception.
* @return Returns {@code true} if requesting the permission was successful, otherwise {@code false}.
*/
@SuppressLint("BatteryLife")
public static void requestDisableBatteryOptimizations(Activity activity, int requestCode) {
public static boolean requestDisableBatteryOptimizations(@NonNull Context context, int requestCode) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, requestCode);
intent.setData(Uri.parse("package:" + context.getPackageName()));
return ActivityUtils.startActivityForResult(context, requestCode, intent);
}
}

View File

@@ -36,9 +36,11 @@
<!-- PermissionUtils -->
<string name="message_sudo_please_grant_permissions">Please grant permissions on next screen</string>
<string name="message_sudo_please_grant_permissions">Please grant requested permission(s)</string>
<string name="error_failed_to_request_permissions">Failed to request permissions with request code %1$d: %2$s</string>
<string name="error_attempted_to_check_for_permissions_not_requested">Attempted to check for permissions that have not been requested in app manifest: %1$s</string>
<string name="error_attempted_to_ask_for_permissions_not_requested">Attempted to ask for permissions that have not been requested in app manifest: %1$s</string>