Skip to content

Commit

Permalink
Added: Add support for getting feature flag values and show MONITOR_P…
Browse files Browse the repository at this point in the history
…HANTOM_PROCS value in about page

MONITOR_PHANTOM_PROCS will only be shown in Android 12+ devices and will be marked "<unsupported>" if its not supported in current android build. It will show in Termux Settings->About->Device Info->Software and in reports. Flag is available on Pixel Android 12L beta 3 and Android 13. Check FeatureFlagUtils for more details.

Getting supported feature flags and their values is done through reflection on android "android.util.FeatureFlagUtils" class and requires bypassing android hidden API restrictions.

Related issue termux#2366
https://issuetracker.google.com/u/1/issues/205156966#comment27
  • Loading branch information
agnostic-apollo committed Feb 12, 2022
1 parent 76cd262 commit f87ba8f
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 0 deletions.
Expand Up @@ -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);
Expand Down
@@ -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("<unknown>"),

/** Flag is unsupported on current android build. */
UNSUPPORTED("<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<String, String> 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<String, String>) 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<String, String> 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;
}
}

}

0 comments on commit f87ba8f

Please sign in to comment.