diff --git a/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java b/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java index 6f2cadd185..766ea34ef9 100644 --- a/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java @@ -120,6 +120,10 @@ public static String getDeviceInfoMarkdownString(@NonNull final Context context) appendPropertyToMarkdown(markdownString, "TYPE", Build.TYPE); appendPropertyToMarkdown(markdownString, "TAGS", Build.TAGS); + // If on Android >= 12 + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) + appendPropertyToMarkdown(markdownString, "MONITOR_PHANTOM_PROCS", FeatureFlagUtils.getFeatureFlagValueString(context, FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS).getName()); + markdownString.append("\n\n### Hardware\n"); appendPropertyToMarkdown(markdownString, "MANUFACTURER", Build.MANUFACTURER); appendPropertyToMarkdown(markdownString, "BRAND", Build.BRAND); diff --git a/termux-shared/src/main/java/com/termux/shared/android/FeatureFlagUtils.java b/termux-shared/src/main/java/com/termux/shared/android/FeatureFlagUtils.java new file mode 100644 index 0000000000..42c1b8017a --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/android/FeatureFlagUtils.java @@ -0,0 +1,181 @@ +package com.termux.shared.android; + +import android.annotation.SuppressLint; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.logger.Logger; +import com.termux.shared.reflection.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * Utils for Developer Options -> Feature Flags. The page won't show in user/production builds and + * is only shown in userdebug builds. + * https://cs.android.com/android/_/android/platform/frameworks/base/+/09dcdad5ebc159861920f090e07da60fac71ac0a:core/java/android/util/FeatureFlagUtils.java + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:packages/apps/Settings/src/com/android/settings/development/featureflags/FeatureFlagsPreferenceController.java;l=42 + * + * The feature flags value can be modified in two ways. + * + * 1. sysprops with `setprop` command with root. Will be unset by default. + * Set value: `setprop persist.sys.fflag.override.settings_enable_monitor_phantom_procs false` + * Get value: `getprop persist.sys.fflag.override.settings_enable_monitor_phantom_procs` + * Unset value: `setprop persist.sys.fflag.override.settings_enable_monitor_phantom_procs ""` + * Running `setprop` command requires root and even adb `shell` user cannot modify the values + * since selinux will not allow it by default. Some props like `settings_dynamic_system` can be + * set since they are exempted for `shell` in sepolicy. + * + * init: Unable to set property 'persist.sys.fflag.override.settings_enable_monitor_phantom_procs' from uid:2000 gid:2000 pid:9576: SELinux permission check failed + * [ 1034.877067] type=1107 audit(1644436809.637:34): uid=0 auid=4294967295 ses=4294967295 subj=u:r:init:s0 msg='avc: denied { set } for property=persist.sys.fflag.override.settings_enable_monitor_phantom_procs pid=9576 uid=2000 gid=2000 scontext=u:r:shell:s0 tcontext=u:object_r:system_prop:s0 tclass=property_service permissive=0' + * + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:system/sepolicy/private/property_contexts;l=71 + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:system/sepolicy/private/shell.te;l=149 + * + * 2. settings global list with adb or root. Will be unset by default. This takes precedence over + * sysprop value since `FeatureFlagUtils.isEnabled()` + * checks its value first. Override precedence: Settings.Global -> sys.fflag.override.* -> static list. + * Set value: `adb shell settings put global settings_enable_monitor_phantom_procs false` + * Get value: adb shell settings get global settings_enable_monitor_phantom_procs` + * Unset value: `adb shell settings delete global settings_enable_monitor_phantom_procs` + * + * https://cs.android.com/android/_/android/platform/frameworks/base/+/refs/tags/android-12.0.0_r31:core/java/android/util/FeatureFlagUtils.java;l=113 + * + * The feature flag values can be modified in user builds with settings global list, but since the + * developer options feature flags page is not shown and considering that getprop values for features + * will be unset by default and settings global list will not be set either and there is no shell API, + * it will require an android app process to check if feature is supported on a device and what its + * default value is with reflection after bypassing hidden api restrictions since {@link #FEATURE_FLAGS_CLASS} + * is annotated as `@hide`. + */ +public class FeatureFlagUtils { + + public enum FeatureFlagValue { + + /** Unknown like due to exception raised while getting value. */ + UNKNOWN(""), + + /** Flag is unsupported on current android build. */ + UNSUPPORTED(""), + + /** Flag is enabled. */ + TRUE("true"), + + /** Flag is not enabled. */ + FALSE("false"); + + private final String name; + + FeatureFlagValue(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + } + + public static final String FEATURE_FLAGS_CLASS = "android.util.FeatureFlagUtils"; + + /** + * If set to false, then will disable trimming of phantom process and processes using excessive + * CPU. Flag is available on Pixel Android 12L beta 3 and Android 13. Availability on other + * devices will depend on if other vendors merged the 09dcdad5 commit or not in their releases + * and if they actually want to support the flag. + * + * https://github.com/termux/termux-app/issues/2366 + * https://issuetracker.google.com/u/1/issues/205156966#comment28 + * https://cs.android.com/android/_/android/platform/frameworks/base/+/09dcdad5 + * https://github.com/agnostic-apollo/Android-Docs/tree/master/ocs/apps/processes/phantom-cached-and-empty-processes.md + */ + public static final String SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS = "settings_enable_monitor_phantom_procs"; + + private static final String LOG_TAG = "FeatureFlagUtils"; + + /** + * Get all feature flags in their raw form. + */ + @SuppressWarnings("unchecked") + public static Map getAllFeatureFlags() { + ReflectionUtils.bypassHiddenAPIReflectionRestrictions(); + try { + @SuppressLint("PrivateApi") Class clazz = Class.forName(FEATURE_FLAGS_CLASS); + Method getAllFeatureFlagsMethod = ReflectionUtils.getDeclaredMethod(clazz, "getAllFeatureFlags"); + if (getAllFeatureFlagsMethod == null) return null; + return (Map) ReflectionUtils.invokeMethod(getAllFeatureFlagsMethod, null).value; + } catch (Exception e) { + // ClassCastException may be thrown + Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get all feature flags", e); + return null; + } + } + + /** + * Check if a feature flag exists. + * + * @return Returns {@code true} if flag exists, otherwise {@code false}. This will be + * {@code null} if an exception is raised. + */ + @Nullable + public static Boolean featureFlagExists(@NonNull String feature) { + Map featureFlags = getAllFeatureFlags(); + if (featureFlags == null) return null; + return featureFlags.containsKey(feature); + } + + /** + * Get {@link FeatureFlagValue} for a feature. + * + * @param context The {@link Context} for operations. + * @param feature The {@link String} name for feature. + * @return Returns {@link FeatureFlagValue}. + */ + public static FeatureFlagValue getFeatureFlagValueString(@NonNull Context context, @NonNull String feature) { + Boolean featureFlagExists = featureFlagExists(feature); + if (featureFlagExists == null) { + Logger.logError(LOG_TAG, "Failed to get feature flags \"" + feature + "\" value"); + return FeatureFlagValue.UNKNOWN; + } else if (!featureFlagExists) { + return FeatureFlagValue.UNSUPPORTED; + } + + Boolean featureFlagValue = isFeatureEnabled(context, feature); + if (featureFlagValue == null) { + Logger.logError(LOG_TAG, "Failed to get feature flags \"" + feature + "\" value"); + return FeatureFlagValue.UNKNOWN; + } else { + return featureFlagValue ? FeatureFlagValue.TRUE : FeatureFlagValue.FALSE; + } + } + + /** + * Check if a feature flag exists. + * + * @param context The {@link Context} for operations. + * @param feature The {@link String} name for feature. + * @return Returns {@code true} if flag exists, otherwise {@code false}. This will be + * {@code null} if an exception is raised. + */ + @Nullable + public static Boolean isFeatureEnabled(@NonNull Context context, @NonNull String feature) { + ReflectionUtils.bypassHiddenAPIReflectionRestrictions(); + try { + @SuppressLint("PrivateApi") Class clazz = Class.forName(FEATURE_FLAGS_CLASS); + Method isFeatureEnabledMethod = ReflectionUtils.getDeclaredMethod(clazz, "isEnabled", Context.class, String.class); + if (isFeatureEnabledMethod == null) { + Logger.logError(LOG_TAG, "Failed to check if feature flag \"" + feature + "\" is enabled"); + return null; + } + + return (boolean) ReflectionUtils.invokeMethod(isFeatureEnabledMethod, null, context, feature).value; + } catch (Exception e) { + // ClassCastException may be thrown + Logger.logStackTraceWithMessage(LOG_TAG, "Failed to check if feature flag \"" + feature + "\" is enabled", e); + return null; + } + } + +}