package com.termux.shared.android; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Service; 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.Environment; import android.os.PowerManager; import android.provider.Settings; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; 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.file.FileUtils; import com.termux.shared.logger.Logger; import com.termux.shared.errors.Error; import com.termux.shared.errors.FunctionErrno; import com.termux.shared.activity.ActivityUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; public class PermissionUtils { public static final int REQUEST_GRANT_STORAGE_PERMISSION = 1000; public static final int REQUEST_DISABLE_BATTERY_OPTIMIZATIONS = 2000; public static final int REQUEST_GRANT_DISPLAY_OVER_OTHER_APPS_PERMISSION = 2001; private static final String LOG_TAG = "PermissionUtils"; /** * 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}); } /** * 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 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 permission : permissions) { result = ContextCompat.checkSelfPermission(context, permission); if (result != PackageManager.PERMISSION_GRANTED) { return false; } } return true; } /** * 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}. */ @RequiresApi(api = Build.VERSION_CODES.M) public static boolean requestPermission(@NonNull Context context, @NonNull String permission, int requestCode) { return requestPermissions(context, new String[]{permission}, requestCode); } /** * 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}. */ @RequiresApi(api = Build.VERSION_CODES.M) public static boolean requestPermissions(@NonNull Context context, @NonNull String[] permissions, int requestCode) { List 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; } for (String permission : permissions) { int result = ContextCompat.checkSelfPermission(context, permission); // If at least one permission not granted if (result != PackageManager.PERMISSION_GRANTED) { Logger.logInfo(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions)); try { if (context instanceof AppCompatActivity) ((AppCompatActivity) context).requestPermissions(permissions, requestCode); else if (context instanceof Activity) ((Activity) context).requestPermissions(permissions, requestCode); else { Error.logErrorAndShowToast(context, LOG_TAG, FunctionErrno.ERRNO_PARAMETER_NOT_INSTANCE_OF.getError("context", "requestPermissions", "Activity or AppCompatActivity")); return false; } } catch (Exception 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; } break; } } return true; } /** * 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} of permissions that have not been requested. It will have * size 0 if all permissions have been requested. */ @NonNull public static List getPermissionsNotRequested(@NonNull Context context, @NonNull String[] permissions) { List 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 requestedPermissionsList = Arrays.asList(packageInfo.requestedPermissions); for (String permission : permissions) { if (requestedPermissionsList.contains(permission)) { permissionsNotRequested.remove(permission); } } return permissionsNotRequested; } /** If path is under primary external storage directory and storage permission is missing, * then legacy or manage external storage permission will be requested from the user via a call * to {@link #checkAndRequestLegacyOrManageExternalStoragePermission(Context, int, boolean)}. * * @param context The context for operations. * @param filePath The path to check. * @param requestCode The request code to use while asking for permission. * @param showErrorMessage If an error message toast should be shown if permission is not granted. * @return Returns {@code true} if permission is granted, otherwise {@code false}. */ @SuppressLint("SdCardPath") public static boolean checkAndRequestLegacyOrManageExternalStoragePermissionIfPathOnPrimaryExternalStorage( @NonNull Context context, String filePath, int requestCode, boolean showErrorMessage) { // If path is under primary external storage directory, then check for missing permissions. if (!FileUtils.isPathInDirPaths(filePath, Arrays.asList(Environment.getExternalStorageDirectory().getAbsolutePath(), "/sdcard"), true)) return true; return checkAndRequestLegacyOrManageExternalStoragePermission(context, requestCode, showErrorMessage); } /** * Check if legacy or manage external storage permissions has been granted. If * {@link #isLegacyExternalStoragePossible(Context)} returns {@code true}, them it will be * checked if app has has been granted {@link Manifest.permission#READ_EXTERNAL_STORAGE} and * {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions, otherwise it will be checked * if app has been granted the {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} permission. * * If storage permission is missing, it will be requested from the user if {@code context} is an * instance of {@link Activity} or {@link AppCompatActivity} and {@code requestCode} * is `>=0` and the function will automatically return. The caller should register for * Activity.onActivityResult() and Activity.onRequestPermissionsResult() and call this function * again but set {@code requestCode} to `-1` to check if permission was granted or not. * * Caller must add following to AndroidManifest.xml of the app, otherwise errors will be thrown. * {@code * * * * * * *} * @param context The context for operations. * @param requestCode The request code to use while asking for permission. * @param showErrorMessage If an error message toast should be shown if permission is not granted. * @return Returns {@code true} if permission is granted, otherwise {@code false}. */ public static boolean checkAndRequestLegacyOrManageExternalStoragePermission(@NonNull Context context, int requestCode, boolean showErrorMessage) { String errmsg; boolean requestLegacyStoragePermission = isLegacyExternalStoragePossible(context); boolean checkIfHasRequestedLegacyExternalStorage = checkIfHasRequestedLegacyExternalStorage(context); if (requestLegacyStoragePermission && checkIfHasRequestedLegacyExternalStorage) { // Check if requestLegacyExternalStorage is set to true in app manifest if (!hasRequestedLegacyExternalStorage(context, showErrorMessage)) return false; } if (checkStoragePermission(context, requestLegacyStoragePermission)) { return true; } errmsg = context.getString(R.string.msg_storage_permission_not_granted); Logger.logError(LOG_TAG, errmsg); if (showErrorMessage) Logger.showToast(context, errmsg, false); if (requestCode < 0 || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false; if (requestLegacyStoragePermission || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { requestLegacyStorageExternalPermission(context, requestCode); } else { requestManageStorageExternalPermission(context, requestCode); } return false; } /** * Check if app has been granted storage permission. * * @param context The context for operations. * @param checkLegacyStoragePermission If set to {@code true}, then it will be checked if app * has been granted {@link Manifest.permission#READ_EXTERNAL_STORAGE} * and {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} * permissions, otherwise it will be checked if app has been * granted the {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} * permission. * @return Returns {@code true} if permission is granted, otherwise {@code false}. */ public static boolean checkStoragePermission(@NonNull Context context, boolean checkLegacyStoragePermission) { if (checkLegacyStoragePermission || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { return checkPermissions(context, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}); } else { return Environment.isExternalStorageManager(); } } /** * Request user to grant {@link Manifest.permission#READ_EXTERNAL_STORAGE} and * {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions 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}. */ @RequiresApi(api = Build.VERSION_CODES.M) public static boolean requestLegacyStorageExternalPermission(@NonNull Context context, int requestCode) { Logger.logInfo(LOG_TAG, "Requesting legacy external storage permission"); return requestPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE, requestCode); } /** Wrapper for {@link #requestManageStorageExternalPermission(Context, int)}. */ @RequiresApi(api = Build.VERSION_CODES.R) public static Error requestManageStorageExternalPermission(@NonNull Context context) { return requestManageStorageExternalPermission(context, -1); } /** * Request user to grant {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} permission to the app. * * @param context The context for operations, like an {@link Activity} or {@link Service} context. * It must be an instance of {@link Activity} or {@link AppCompatActivity} if * result is required via the Activity#onActivityResult() callback and * {@code requestCode} is `>=0`. * @param requestCode The request code to use while asking for permission. It must be `>=0` if * result it required. * @return Returns the {@code error} if requesting the permission was not successful, otherwise {@code null}. */ @RequiresApi(api = Build.VERSION_CODES.R) public static Error requestManageStorageExternalPermission(@NonNull Context context, int requestCode) { Logger.logInfo(LOG_TAG, "Requesting manage external storage permission"); Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); intent.addCategory("android.intent.category.DEFAULT"); intent.setData(Uri.parse("package:" + context.getPackageName())); Error error; if (requestCode >=0) error = ActivityUtils.startActivityForResult(context, requestCode, intent, true, false); else error = ActivityUtils.startActivity(context, intent, true, false); // Use fallback if matching Activity did not exist for ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION. if (error != null) { intent = new Intent(); intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); if (requestCode >=0) return ActivityUtils.startActivityForResult(context, requestCode, intent); else return ActivityUtils.startActivity(context, intent); } return null; } /** * If app is targeting targetSdkVersion 30 (android 11) and running on sdk 30 (android 11) or * higher, then {@link android.R.attr#requestLegacyExternalStorage} attribute is ignored. * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage */ public static boolean isLegacyExternalStoragePossible(@NonNull Context context) { return !(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && PackageUtils.getTargetSDKForPackage(context) >= Build.VERSION_CODES.R); } /** * Return whether it should be checked if app has set * {@link android.R.attr#requestLegacyExternalStorage} attribute to {@code true}, if storage * permissions are to be requested based on if {@link #isLegacyExternalStoragePossible(Context)} * return {@code true}. * * If app is targeting targetSdkVersion 30 (android 11), then legacy storage can only be * requested if running on sdk 29 (android 10). * If app is targeting targetSdkVersion 29 (android 10), then legacy storage can only be * requested if running on sdk 29 (android 10) and higher. */ public static boolean checkIfHasRequestedLegacyExternalStorage(@NonNull Context context) { int targetSdkVersion = PackageUtils.getTargetSDKForPackage(context); if (targetSdkVersion >= Build.VERSION_CODES.R) { return Build.VERSION.SDK_INT == Build.VERSION_CODES.Q; } else if (targetSdkVersion == Build.VERSION_CODES.Q) { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; } else { return false; } } /** * Call to {@link Environment#isExternalStorageLegacy()} will not return the actual value defined * in app manifest for {@link android.R.attr#requestLegacyExternalStorage} attribute, * since an app may inherit its legacy state based on when it was first installed, target sdk and * other factors. To provide consistent experience for all users regardless of current legacy * state on a specific device, we directly use the value defined in app` manifest. */ public static boolean hasRequestedLegacyExternalStorage(@NonNull Context context, boolean showErrorMessage) { String errmsg; Boolean hasRequestedLegacyExternalStorage = PackageUtils.hasRequestedLegacyExternalStorage(context); if (hasRequestedLegacyExternalStorage != null && !hasRequestedLegacyExternalStorage) { errmsg = context.getString(R.string.error_has_not_requested_legacy_external_storage, context.getPackageName(), PackageUtils.getTargetSDKForPackage(context), Build.VERSION.SDK_INT); Logger.logError(LOG_TAG, errmsg); if (showErrorMessage) Logger.showToast(context, errmsg, true); return false; } return true; } /** * 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) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) return Settings.canDrawOverlays(context); else return true; } /** Wrapper for {@link #requestDisplayOverOtherAppsPermission(Context, int)}. */ public static Error requestDisplayOverOtherAppsPermission(@NonNull Context context) { return requestDisplayOverOtherAppsPermission(context, -1); } /** * Request user to grant {@link Manifest.permission#SYSTEM_ALERT_WINDOW} permission to the app. * * @param context The context for operations, like an {@link Activity} or {@link Service} context. * It must be an instance of {@link Activity} or {@link AppCompatActivity} if * result is required via the Activity#onActivityResult() callback and * {@code requestCode} is `>=0`. * @param requestCode The request code to use while asking for permission. It must be `>=0` if * result it required. * @return Returns the {@code error} if requesting the permission was not successful, otherwise {@code null}. */ public static Error requestDisplayOverOtherAppsPermission(@NonNull Context context, int requestCode) { Logger.logInfo(LOG_TAG, "Requesting display over apps permission"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return null; Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); intent.setData(Uri.parse("package:" + context.getPackageName())); if (requestCode >=0) return ActivityUtils.startActivityForResult(context, requestCode, intent); else return ActivityUtils.startActivity(context, intent); } /** * 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 (!checkDisplayOverOtherAppsPermission(context)) { if (logResults) Logger.logWarn(LOG_TAG, context.getPackageName() + " does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission"); return false; } else { if (logResults) Logger.logDebug(LOG_TAG, context.getPackageName() + " already has Display over other apps (SYSTEM_ALERT_WINDOW) permission"); return true; } } /** * 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); return powerManager.isIgnoringBatteryOptimizations(context.getPackageName()); } else return true; } /** Wrapper for {@link #requestDisableBatteryOptimizations(Context, int)}. */ public static Error requestDisableBatteryOptimizations(@NonNull Context context) { return requestDisableBatteryOptimizations(context, -1); } /** * Request user to grant {@link Manifest.permission#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS} * permission to the app. * * @param context The context for operations, like an {@link Activity} or {@link Service} context. * It must be an instance of {@link Activity} or {@link AppCompatActivity} if * result is required via the Activity#onActivityResult() callback and * {@code requestCode} is `>=0`. * @param requestCode The request code to use while asking for permission. It must be `>=0` if * result it required. * @return Returns the {@code error} if requesting the permission was not successful, otherwise {@code null}. */ @SuppressLint("BatteryLife") public static Error requestDisableBatteryOptimizations(@NonNull Context context, int requestCode) { Logger.logInfo(LOG_TAG, "Requesting to disable battery optimizations"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return null; Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse("package:" + context.getPackageName())); if (requestCode >=0) return ActivityUtils.startActivityForResult(context, requestCode, intent); else return ActivityUtils.startActivity(context, intent); } }