Changed!: Move to package-by-feature hierarchy for classes not using it since termux-shared is growing too big and layers are getting out of hand

This commit is contained in:
agnostic-apollo
2021-10-26 07:04:08 +05:00
parent 549a772d45
commit 361bfb3961
47 changed files with 105 additions and 174 deletions

View File

@@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
import com.google.common.base.Joiner;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.packages.PackageUtils;
import java.io.BufferedReader;
import java.io.IOException;

View File

@@ -0,0 +1,714 @@
package com.termux.shared.android;
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.os.UserManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.reflection.ReflectionUtils;
import com.termux.shared.termux.TermuxConstants;
import java.lang.reflect.Field;
import java.security.MessageDigest;
import java.util.List;
public class PackageUtils {
private static final String LOG_TAG = "PackageUtils";
/**
* Get the {@link Context} for the package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
* @param packageName The package name whose {@link Context} to get.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
@Nullable
public static Context getContextForPackage(@NonNull final Context context, String packageName) {
try {
return context.createPackageContext(packageName, Context.CONTEXT_RESTRICTED);
} catch (Exception e) {
Logger.logVerbose(LOG_TAG, "Failed to get \"" + packageName + "\" package context: " + e.getMessage());
return null;
}
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
* @param packageName The package name whose {@link Context} to get.
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
* be shown which when dismissed will exit the app.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
@Nullable
public static Context getContextForPackageOrExitApp(@NonNull Context context, String packageName, final boolean exitAppOnError) {
Context packageContext = getContextForPackage(context, packageName);
if (packageContext == null && exitAppOnError) {
String errorMessage = context.getString(R.string.error_get_package_context_failed_message,
packageName, TermuxConstants.TERMUX_GITHUB_REPO_URL);
Logger.logError(LOG_TAG, errorMessage);
MessageDialogUtils.exitAppWithErrorMessage(context,
context.getString(R.string.error_get_package_context_failed_title),
errorMessage);
}
return packageContext;
}
/**
* Get the {@link PackageInfo} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
*/
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context) {
return getPackageInfoForPackage(context, context.getPackageName());
}
/**
* Get the {@link PackageInfo} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @param flags The flags to pass to {@link PackageManager#getPackageInfo(String, int)}.
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context, final int flags) {
return getPackageInfoForPackage(context, context.getPackageName(), flags);
}
/**
* Get the {@link PackageInfo} for the package associated with the {@code packageName}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the package.
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
*/
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context, @NonNull final String packageName) {
return getPackageInfoForPackage(context, packageName, 0);
}
/**
* Get the {@link PackageInfo} for the package associated with the {@code packageName}.
*
* Also check {@link #isAppInstalled(Context, String, String) if targetting targeting sdk
* `30` (android `11`) since {@link PackageManager.NameNotFoundException} may be thrown.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the package.
* @param flags The flags to pass to {@link PackageManager#getPackageInfo(String, int)}.
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context, @NonNull final String packageName, final int flags) {
try {
return context.getPackageManager().getPackageInfo(packageName, flags);
} catch (final Exception e) {
return null;
}
}
/**
* Get the {@link ApplicationInfo} for the {@code packageName}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the package.
* @return Returns the {@link ApplicationInfo}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static ApplicationInfo getApplicationInfoForPackage(@NonNull final Context context, @NonNull final String packageName) {
return getApplicationInfoForPackage(context, packageName, 0);
}
/**
* Get the {@link ApplicationInfo} for the {@code packageName}.
*
* Also check {@link #isAppInstalled(Context, String, String) if targetting targeting sdk
* `30` (android `11`) since {@link PackageManager.NameNotFoundException} may be thrown.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the package.
* @param flags The flags to pass to {@link PackageManager#getApplicationInfo(String, int)}.
* @return Returns the {@link ApplicationInfo}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static ApplicationInfo getApplicationInfoForPackage(@NonNull final Context context, @NonNull final String packageName, final int flags) {
try {
return context.getPackageManager().getApplicationInfo(packageName, flags);
} catch (final Exception e) {
return null;
}
}
/**
* Get the {@code privateFlags} {@link Field} of the {@link ApplicationInfo} class.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the private flags or {@code null} if an exception was raised.
*/
@Nullable
public static Integer getApplicationInfoPrivateFlagsForPackage(@NonNull final ApplicationInfo applicationInfo) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
try {
return (Integer) ReflectionUtils.invokeField(ApplicationInfo.class, "privateFlags", applicationInfo).value;
} catch (Exception e) {
// ClassCastException may be thrown
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get privateFlags field value for ApplicationInfo class", e);
return null;
}
}
/**
* Get the {@code privateFlags} {@link Field} of the {@link ApplicationInfo} class.
*
* @param fieldName The name of the field to get.
* @return Returns the field value or {@code null} if an exception was raised.
*/
@Nullable
public static Integer getApplicationInfoStaticIntFieldValue(@NonNull String fieldName) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
try {
return (Integer) ReflectionUtils.invokeField(ApplicationInfo.class, fieldName, null).value;
} catch (Exception e) {
// ClassCastException may be thrown
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field value for ApplicationInfo class", e);
return null;
}
}
/**
* Check if the app associated with the {@code applicationInfo} has a specific flag set.
*
* @param flagToCheckName The name of the field for the flag to check.
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns {@code true} if app has flag is set, otherwise {@code false}. This will be
* {@code null} if an exception is raised.
*/
@Nullable
public static Boolean isApplicationInfoPrivateFlagSetForPackage(@NonNull String flagToCheckName, @NonNull final ApplicationInfo applicationInfo) {
Integer privateFlags = getApplicationInfoPrivateFlagsForPackage(applicationInfo);
if (privateFlags == null) return null;
Integer flagToCheck = getApplicationInfoStaticIntFieldValue(flagToCheckName);
if (flagToCheck == null) return null;
return ( 0 != ( privateFlags & flagToCheck ) );
}
/**
* Get the app name for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code android:name} attribute.
*/
public static String getAppNameForPackage(@NonNull final Context context) {
return getAppNameForPackage(context, context.getApplicationInfo());
}
/**
* Get the app name for the package associated with the {@code applicationInfo}.
*
* @param context The {@link Context} for operations.
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the {@code android:name} attribute.
*/
public static String getAppNameForPackage(@NonNull final Context context, @NonNull final ApplicationInfo applicationInfo) {
return applicationInfo.loadLabel(context.getPackageManager()).toString();
}
/**
* Get the package name for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the package name.
*/
public static String getPackageNameForPackage(@NonNull final Context context) {
return getPackageNameForPackage(context.getApplicationInfo());
}
/**
* Get the package name for the package associated with the {@code applicationInfo}.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the package name.
*/
public static String getPackageNameForPackage(@NonNull final ApplicationInfo applicationInfo) {
return applicationInfo.packageName;
}
/**
* Get the uid for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the uid.
*/
public static int getUidForPackage(@NonNull final Context context) {
return getUidForPackage(context.getApplicationInfo());
}
/**
* Get the uid for the package associated with the {@code applicationInfo}.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the uid.
*/
public static int getUidForPackage(@NonNull final ApplicationInfo applicationInfo) {
return applicationInfo.uid;
}
/**
* Get the {@code targetSdkVersion} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code targetSdkVersion}.
*/
public static int getTargetSDKForPackage(@NonNull final Context context) {
return getTargetSDKForPackage(context.getApplicationInfo());
}
/**
* Get the {@code targetSdkVersion} for the package associated with the {@code applicationInfo}.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the {@code targetSdkVersion}.
*/
public static int getTargetSDKForPackage(@NonNull final ApplicationInfo applicationInfo) {
return applicationInfo.targetSdkVersion;
}
/**
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_DEBUGGABLE}
* set.
*
* @param context The {@link Context} for the package.
* @return Returns {@code true} if app is debuggable, otherwise {@code false}.
*/
public static boolean isAppForPackageADebuggableBuild(@NonNull final Context context) {
return isAppForPackageADebuggableBuild(context.getApplicationInfo());
}
/**
* Check if the app associated with the {@code applicationInfo} has {@link ApplicationInfo#FLAG_DEBUGGABLE}
* set.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns {@code true} if app is debuggable, otherwise {@code false}.
*/
public static boolean isAppForPackageADebuggableBuild(@NonNull final ApplicationInfo applicationInfo) {
return ( 0 != ( applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
}
/**
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_EXTERNAL_STORAGE}
* set.
*
* @param context The {@link Context} for the package.
* @return Returns {@code true} if app is installed on external storage, otherwise {@code false}.
*/
public static boolean isAppInstalledOnExternalStorage(@NonNull final Context context) {
return isAppInstalledOnExternalStorage(context.getApplicationInfo());
}
/**
* Check if the app associated with the {@code applicationInfo} has {@link ApplicationInfo#FLAG_EXTERNAL_STORAGE}
* set.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns {@code true} if app is installed on external storage, otherwise {@code false}.
*/
public static boolean isAppInstalledOnExternalStorage(@NonNull final ApplicationInfo applicationInfo) {
return ( 0 != ( applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE ) );
}
/**
* Check if the app associated with the {@code context} has
* ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE (requestLegacyExternalStorage)
* set to {@code true} in app manifest.
*
* @param context The {@link Context} for the package.
* @return Returns {@code true} if app has requested legacy external storage, otherwise
* {@code false}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static Boolean hasRequestedLegacyExternalStorage(@NonNull final Context context) {
return hasRequestedLegacyExternalStorage(context.getApplicationInfo());
}
/**
* Check if the app associated with the {@code applicationInfo} has
* ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE (requestLegacyExternalStorage)
* set to {@code true} in app manifest.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns {@code true} if app has requested legacy external storage, otherwise
* {@code false}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static Boolean hasRequestedLegacyExternalStorage(@NonNull final ApplicationInfo applicationInfo) {
return isApplicationInfoPrivateFlagSetForPackage("PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE", applicationInfo);
}
/**
* Get the {@code versionCode} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionCode}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static Integer getVersionCodeForPackage(@NonNull final Context context) {
return getVersionCodeForPackage(context, context.getPackageName());
}
/**
* Get the {@code versionCode} for the {@code packageName}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the package.
* @return Returns the {@code versionCode}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static Integer getVersionCodeForPackage(@NonNull final Context context, @NonNull final String packageName) {
return getVersionCodeForPackage(getPackageInfoForPackage(context, packageName));
}
/**
* Get the {@code versionCode} for the {@code packageName}.
*
* @param packageInfo The {@link PackageInfo} for the package.
* @return Returns the {@code versionCode}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static Integer getVersionCodeForPackage(@Nullable final PackageInfo packageInfo) {
return packageInfo != null ? packageInfo.versionCode : null;
}
/**
* Get the {@code versionName} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getVersionNameForPackage(@NonNull final Context context) {
return getVersionNameForPackage(context, context.getPackageName());
}
/**
* Get the {@code versionName} for the {@code packageName}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getVersionNameForPackage(@NonNull final Context context, @NonNull final String packageName) {
return getVersionNameForPackage(getPackageInfoForPackage(context, packageName));
}
/**
* Get the {@code versionName} for the {@code packageName}.
*
* @param packageInfo The {@link PackageInfo} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an {@code packageInfo}
* is {@code null}.
*/
@Nullable
public static String getVersionNameForPackage(@Nullable final PackageInfo packageInfo) {
return packageInfo != null ? packageInfo.versionName : null;
}
/**
* Get the {@code SHA-256 digest} of signing certificate for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code SHA-256 digest}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context) {
return getSigningCertificateSHA256DigestForPackage(context, context.getPackageName());
}
/**
* Get the {@code SHA-256 digest} of signing certificate for the {@code packageName}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the package.
* @return Returns the {@code SHA-256 digest}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context, @NonNull final String packageName) {
try {
/*
* Todo: We may need AndroidManifest queries entries if package is installed but with a different signature on android 11
* https://developer.android.com/training/package-visibility
* Need a device that allows (manual) installation of apk with mismatched signature of
* sharedUserId apps to test. Currently, if its done, PackageManager just doesn't load
* the package and removes its apk automatically if its installed as a user app instead of system app
* W/PackageManager: Failed to parse /path/to/com.termux.tasker.apk: Signature mismatch for shared user: SharedUserSetting{xxxxxxx com.termux/10xxx}
*/
PackageInfo packageInfo = getPackageInfoForPackage(context, packageName, PackageManager.GET_SIGNATURES);
if (packageInfo == null) return null;
return DataUtils.bytesToHex(MessageDigest.getInstance("SHA-256").digest(packageInfo.signatures[0].toByteArray()));
} catch (final Exception e) {
return null;
}
}
/**
* Get the serial number for the user for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the serial number. This will be {@code null} if failed to get it.
*/
@Nullable
public static Long getUserIdForPackage(@NonNull Context context) {
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (userManager == null) return null;
return userManager.getSerialNumberForUser(UserHandle.getUserHandleForUid(getUidForPackage(context)));
}
/**
* Check if the current user is the primary user. This is done by checking if the the serial
* number for the current user equals 0.
*
* @param context The {@link Context} for operations.
* @return Returns {@code true} if the current user is the primary user, otherwise [@code false}.
*/
public static boolean isCurrentUserThePrimaryUser(@NonNull Context context) {
Long userId = getUserIdForPackage(context);
return userId != null && userId == 0;
}
/**
* Get the profile owner package name for the current user.
*
* @param context The {@link Context} for operations.
* @return Returns the profile owner package name. This will be {@code null} if failed to get it
* or no profile owner for the current user.
*/
@Nullable
public static String getProfileOwnerPackageNameForUser(@NonNull Context context) {
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
if (devicePolicyManager == null) return null;
List<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
if (activeAdmins != null){
for (ComponentName admin:activeAdmins){
String packageName = admin.getPackageName();
if(devicePolicyManager.isProfileOwnerApp(packageName))
return packageName;
}
}
return null;
}
/**
* Get the process id of the main app process of a package. This will work for sharedUserId. Note
* that some apps have multiple processes for the app like with `android:process=":background"`
* attribute in AndroidManifest.xml.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the process.
* @return Returns the process if found and running, otherwise {@code null}.
*/
@Nullable
public static String getPackagePID(final Context context, String packageName) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager != null) {
List<ActivityManager.RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
if (processInfos != null) {
ActivityManager.RunningAppProcessInfo processInfo;
for (int i = 0; i < processInfos.size(); i++) {
processInfo = processInfos.get(i);
if (processInfo.processName.equals(packageName))
return String.valueOf(processInfo.pid);
}
}
}
return null;
}
/**
* Check if app is installed and enabled. This can be used by external apps that don't
* share `sharedUserId` with the an app.
*
* If your third-party app is targeting sdk `30` (android `11`), then it needs to add package
* name to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... package_name/......} BLOCKED`
* errors in `logcat` and {@link PackageManager.NameNotFoundException} may be thrown.
* `RUN_COMMAND` intent won't work either.
* Check [package-visibility](https://developer.android.com/training/basics/intents/package-visibility#package-name),
* `QUERY_ALL_PACKAGES` [googleplay policy](https://support.google.com/googleplay/android-developer/answer/10158779
* and this [article](https://medium.com/androiddevelopers/working-with-package-visibility-dc252829de2d) for more info.
*
* {@code
* <manifest
* <queries>
* <package android:name="com.termux" />
* </queries>
*
* <application
* ....
* </application>
* </manifest>
* }
*
* @param context The context for operations.
* @param appName The name of the app.
* @param packageName The package name of the package.
* @return Returns {@code errmsg} if {@code packageName} is not installed or disabled, otherwise {@code null}.
*/
public static String isAppInstalled(@NonNull final Context context, String appName, String packageName) {
String errmsg = null;
ApplicationInfo applicationInfo = getApplicationInfoForPackage(context, packageName);
boolean isAppEnabled = (applicationInfo != null && applicationInfo.enabled);
// If app is not installed or is disabled
if (!isAppEnabled)
errmsg = context.getString(R.string.error_app_not_installed_or_disabled_warning, appName, packageName);
return errmsg;
}
/**
* Enable or disable a {@link ComponentName} with a call to
* {@link PackageManager#setComponentEnabledSetting(ComponentName, int, int)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param state If component should be enabled or disabled.
* @param toastString If this is not {@code null} or empty, then a toast before setting state.
* @param showErrorMessage If an error message toast should be shown.
* @return Returns the errmsg if failed to set state, otherwise {@code null}.
*/
@Nullable
public static String setComponentState(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, boolean state, String toastString,
boolean showErrorMessage) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
ComponentName componentName = new ComponentName(packageName, className);
if (toastString != null) Logger.showToast(context, toastString, true);
packageManager.setComponentEnabledSetting(componentName,
state ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
return null;
} catch (final Exception e) {
String errmsg = context.getString(
state ? R.string.error_enable_component_failed : R.string.error_disable_component_failed,
packageName, className) + ": " + e.getMessage();
if (showErrorMessage)
Logger.showToast(context, errmsg, true);
return errmsg;
}
}
/**
* Check if state of a {@link ComponentName} is {@link PackageManager#COMPONENT_ENABLED_STATE_DISABLED}
* with a call to {@link PackageManager#getComponentEnabledSetting(ComponentName)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param logErrorMessage If an error message should be logged.
* @return Returns {@code true} if disabled, {@code false} if not and {@code null} if failed to
* get the state.
*/
public static Boolean isComponentDisabled(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, boolean logErrorMessage) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
ComponentName componentName = new ComponentName(packageName, className);
// Will throw IllegalArgumentException: Unknown component: ComponentInfo{} if app
// for context is not installed or component does not exist.
return packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
}
} catch (final Exception e) {
if (logErrorMessage)
Logger.logStackTraceWithMessage(LOG_TAG, context.getString(R.string.error_get_component_state_failed, packageName, className), e);
}
return null;
}
/**
* Check if an {@link android.app.Activity} {@link ComponentName} can be called by calling
* {@link PackageManager#queryIntentActivities(Intent, int)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param flags The flags to filter results.
* @return Returns {@code true} if it exists, otherwise {@code false}.
*/
public static boolean doesActivityComponentExist(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, int flags) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
Intent intent = new Intent();
intent.setClassName(packageName, className);
return packageManager.queryIntentActivities(intent, flags).size() > 0;
}
} catch (final Exception e) {
// ignore
}
return false;
}
}

View File

@@ -0,0 +1,495 @@
package com.termux.shared.android;
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.Environment;
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.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.errors.Error;
import com.termux.shared.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 {
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<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 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}.
*/
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}.
*/
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;
}
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(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) {
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;
}
/**
* 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;
}
/** 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
* <manifest
* <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
* <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
* <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
*
* <application
* android:requestLegacyExternalStorage="true"
* ....
* </application>
* </manifest>
*}
* @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)
return false;
if (requestLegacyStoragePermission) {
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) {
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}.
*/
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);
}
/**
* Request user to grant {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} 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 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()));
boolean result = ActivityUtils.startActivityForResult(context, requestCode, intent);
// Use fallback if matching Activity did not exist for ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION.
if (!result) {
intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
return ActivityUtils.startActivityForResult(context, requestCode, intent);
}
return true;
}
/**
* 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 !(PackageUtils.getTargetSDKForPackage(context) >= Build.VERSION_CODES.R &&
Build.VERSION.SDK_INT >= 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) {
return Settings.canDrawOverlays(context);
}
/**
* 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);
}
/**
* 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) {
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 boolean requestDisableBatteryOptimizations(@NonNull Context context, int requestCode) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + context.getPackageName()));
return ActivityUtils.startActivityForResult(context, requestCode, intent);
}
}