patrickfav/under-the-hood

View on GitHub
hood-core/src/main/java/at/favre/lib/hood/util/defaults/DefaultProperties.java

Summary

Maintainability
F
5 days
Test Coverage
/*
 *  Copyright 2016 Patrick Favre-Bulle
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package at.favre.lib.hood.util.defaults;

import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ConfigurationInfo;
import android.content.pm.PackageManager;
import android.net.TrafficStats;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Debug;
import android.os.PowerManager;
import android.os.StrictMode;
import android.os.SystemClock;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.DisplayMetrics;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;

import at.favre.lib.hood.Hood;
import at.favre.lib.hood.interfaces.PageEntry;
import at.favre.lib.hood.interfaces.Section;
import at.favre.lib.hood.interfaces.values.DynamicValue;
import at.favre.lib.hood.util.DeviceStatusUtil;
import at.favre.lib.hood.util.HoodUtil;
import at.favre.lib.hood.util.PermissionTranslator;
import at.favre.lib.hood.util.TypeTranslators;
import timber.log.Timber;

/**
 * A set of methods that returns default {@link at.favre.lib.hood.interfaces.HoodAPI#createPropertyEntry(CharSequence, DynamicValue)} type page entries.
 */
public final class DefaultProperties {

    private static final String TAG = DefaultProperties.class.getName();

    private DefaultProperties() {
    }

    /**
     * Returns entries of some basic device data like model number and sdk version.
     *
     * @return list of entries
     */
    public static Section.HeaderSection createSectionBasicDeviceInfo() {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Device");
        section.add(Hood.get().createPropertyEntry("model", Build.MODEL));
        section.add(Hood.get().createPropertyEntry("name", Build.DEVICE));
        section.add(Hood.get().createPropertyEntry("brand", Build.MANUFACTURER));
        section.add(Hood.get().createPropertyEntry("version", Build.VERSION.RELEASE));
        section.add(Hood.get().createPropertyEntry("version_minor", Build.VERSION.INCREMENTAL));
        section.add(Hood.get().createPropertyEntry("build-id", Build.ID));
        section.add(Hood.get().createPropertyEntry("sdk-int", String.valueOf(Build.VERSION.SDK_INT)));
        section.add(Hood.get().createPropertyEntry("serial", Build.SERIAL));

        return section;
    }

    /**
     * Additional device info
     *
     * @param activity can be null, but will just return an empty list
     * @return list of page-entries
     */
    public static List<PageEntry<?>> createDetailedDeviceInfo(@Nullable final Activity activity) {
        List<PageEntry<?>> entries = new ArrayList<>();
        if (activity != null) {
            DisplayMetrics metrics = new DisplayMetrics();
            activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
            entries.add(Hood.get().createPropertyEntry("dpi", "x" + metrics.density + "/" + metrics.densityDpi + "dpi"));
            entries.add(Hood.get().createPropertyEntry("resolution", metrics.heightPixels + "x" + metrics.widthPixels));
            entries.add(Hood.get().createPropertyEntry("locale/timezone", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    return HoodUtil.getCurrentLocale(activity).toString() + "/" + TimeZone.getDefault().getDisplayName();
                }
            }));
            entries.add(Hood.get().createPropertyEntry("uptime", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    return HoodUtil.millisToDaysHoursMinString(SystemClock.elapsedRealtime());
                }
            }));
            entries.add(Hood.get().createPropertyEntry("android-id", Settings.Secure.getString(activity.getContentResolver(), Settings.Secure.ANDROID_ID)));
        }
        return entries;
    }

    /**
     * Very technical memory & classloader process states - this data is usually very volatile
     *
     * @param context
     * @return section for info
     */
    public static Section.HeaderSection createInternalProcessDebugInfo(@Nullable final Context context) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Process Debug Info");

        if (context != null) {
            section.add(Hood.get().createPropertyEntry("heap-native", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    return "used " + HoodUtil.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false) + " of " + HoodUtil.humanReadableByteCount(Debug.getNativeHeapSize(), false);
                }
            }));

            section.add(Hood.get().createPropertyEntry("memory", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    return "used " + HoodUtil.humanReadableByteCount(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), false) + " (max:" + HoodUtil.humanReadableByteCount(Runtime.getRuntime().maxMemory(), false) + ")";
                }
            }));
            section.add(Hood.get().createPropertyEntry("pss", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    return HoodUtil.humanReadableByteCount(Debug.getPss(), false);
                }
            }));
            section.add(Hood.get().createPropertyEntry("loaded-classes", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    return String.valueOf(Debug.getLoadedClassCount());
                }
            }));
            section.add(Hood.get().createPropertyEntry("local/death/proxy objs", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    return Debug.getBinderLocalObjectCount() + "/" + Debug.getBinderDeathObjectCount() + "/" + Debug.getBinderProxyObjectCount();
                }
            }));
            section.add(Hood.get().createPropertyEntry("debugger-connected", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    return String.valueOf(Debug.isDebuggerConnected());
                }
            }));
        }
        return section;
    }

    /**
     * Traverses the static fields of given arbitrary class. Will create an entry for each non-null
     * static public field. Ignores "serialVersionUID".
     * <p>
     * NOTE: this uses reflection to traverse to class, so if you want to keep it after proguard, use
     * a keep rule like:
     * <p>
     * <pre>
     *     -keep public class your.package.name.BuildConfig { public *;}
     * </pre>
     *
     * @param clazz the BuildConfig.java class you want the info
     * @return list of page-entries
     */
    public static List<PageEntry<?>> createStaticFieldsInfo(Class<?> clazz) {
        List<PageEntry<?>> entries = new ArrayList<>();

        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            if (Modifier.isStatic(field.getModifiers()))
                try {
                    String key = field.getName();
                    String value = String.valueOf(field.get(null));

                    if (key != null && !key.equals("serialVersionUID") &&
                            value != null && !value.equals("null") && !value.trim().isEmpty()) {
                        entries.add(Hood.get().createPropertyEntry(key, String.valueOf(field.get(null))));
                    }
                } catch (Exception e) {
                    Timber.w("could not get field from class (" + field + ")");
                }
        }

        return entries;
    }

    /**
     * Traverses the static fields of given class (which must be of Type BuildConfig) and parses the main
     * fields that is create by the android gradle plugin (ie. version, app_id, etc.)
     * NOTE: this uses reflection to traverse to class, so if you want to keep it after proguard, use
     * a keep rule like:
     * <p>
     * <pre>
     *     -keep public class your.package.name.BuildConfig { public *;}
     * </pre>
     *
     * @param buildConfig the BuildConfig.java class you want the info
     * @return the section containing header, entries etc.
     */
    public static Section.HeaderSection createSectionAppVersionInfoFromBuildConfig(Class<?> buildConfig) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("App Version");
        Field[] declaredFields = buildConfig.getDeclaredFields();
        for (Field field : declaredFields) {
            if (Modifier.isStatic(field.getModifiers())) {
                try {
                    String key = null;
                    String value = String.valueOf(field.get(null));
                    switch (field.getName().toLowerCase()) {
                        case "version_name":
                            key = "version";
                            break;
                        case "version_code":
                            key = "version-code";
                            break;
                        case "application_id":
                            key = "app-id";
                            break;
                        case "build_type":
                            key = "build-type";
                            break;
                        case "flavor":
                            key = "flavor";
                            break;
                        case "debug":
                            key = "debug";
                            break;
                    }

                    if (key != null && value != null && !value.trim().isEmpty()) {
                        section.add(Hood.get().createPropertyEntry(key, String.valueOf(field.get(null))));
                    }

                } catch (Exception e) {
                    Timber.w("could not get field from BuildConfig (" + field + ")");
                }
            }
        }

        return section;
    }

    /**
     * Returns for each provided permission a page-entry containing the current dynamic state (granted, denied, etc.) including click
     * actions to request the permission.
     *
     * @param activity           can be null, but will just return an empty list
     * @param androidPermissions see {@link android.Manifest.permission}
     * @return the section containing header, entries etc.
     */
    public static Section.HeaderSection createSectionRuntimePermissions(@Nullable final Activity activity, List<String> androidPermissions) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Permissions");

        if (activity != null && !androidPermissions.isEmpty()) {
            for (final String perm : androidPermissions) {
                section.add(Hood.get().createPropertyEntry(perm.replace("android.permission.", ""), new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return TypeTranslators.translatePermissionState(PermissionTranslator.getPermissionStatus(activity, perm));
                    }
                }, Hood.ext().createOnClickActionAskPermission(perm, activity), false));
            }
        }
        return section;
    }

    /**
     * Get current state of various connectivity adapters (network, wifi, bt, nfc,...). Requires to have the specific
     * permission to be able to show the actual state (see eg. {@link DeviceStatusUtil#getNetworkConnectivityState(Context)}.
     * <p>
     * Click action is opening the system settings for each adapter.
     *
     * @param context can be null, but will just return an empty list
     * @return the section containing header, entries etc.
     */
    public static Section.HeaderSection createSectionConnectivityStatusInfo(@Nullable final Context context) {
        return createSectionConnectivityStatusInfo(context, true, true, true, true);
    }

    /**
     * See {@link #createSectionConnectivityStatusInfo(Context)}
     *
     * @param context             can be null, but will just return an empty list
     * @param includeNetworkState if network state should be included
     * @param includeWifiState    if wifi state should be included
     * @param includeBtState      if bluetooth state should be included
     * @param includeNfcState     if nfc state should be included
     * @return the section containing header, entries etc.
     */
    public static Section.HeaderSection createSectionConnectivityStatusInfo(@Nullable final Context context, boolean includeNetworkState, boolean includeWifiState, boolean includeBtState, boolean includeNfcState) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Connectivity Status");

        if (context != null) {
            if (includeNetworkState) {
                section.add(Hood.get().createPropertyEntry("internet", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return String.valueOf(DeviceStatusUtil.getNetworkConnectivityState(context));
                    }
                }, Hood.ext().createOnClickActionStartIntent(new Intent(Settings.ACTION_SETTINGS)), false));
            }

            if (includeWifiState) {
                final DeviceStatusUtil.Status wifiState = DeviceStatusUtil.getWifiStatus(context);
                section.add(Hood.get().createPropertyEntry("wifi", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return String.valueOf(wifiState);
                    }
                }, wifiState == DeviceStatusUtil.Status.UNSUPPORTED ?
                        Hood.ext().createOnClickActionToast() :
                        Hood.ext().createOnClickActionStartIntent(new Intent(Settings.ACTION_WIFI_SETTINGS)), false));
            }

            if (includeBtState) {
                final DeviceStatusUtil.Status btState = DeviceStatusUtil.getBluetoothStatus(context);
                section.add(Hood.get().createPropertyEntry("bluetooth", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return String.valueOf(btState);
                    }
                }, btState == DeviceStatusUtil.Status.UNSUPPORTED ?
                        Hood.ext().createOnClickActionToast() :
                        Hood.ext().createOnClickActionStartIntent(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)), false));
            }

            if (includeNfcState) {
                final DeviceStatusUtil.Status nfcState = DeviceStatusUtil.getNfcState(context);
                section.add(Hood.get().createPropertyEntry("nfc", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return String.valueOf(nfcState);
                    }
                }, nfcState == DeviceStatusUtil.Status.UNSUPPORTED || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN ?
                        Hood.ext().createOnClickActionToast() :
                        Hood.ext().createOnClickActionStartIntent(new Intent(Settings.ACTION_NFC_SETTINGS)), false));
            }
        }
        return section;
    }

    /**
     * Convince feature to add state of multiple system features.
     * Uses {@link PackageManager#hasSystemFeature(String)} call.
     * <p>
     * See https://developer.android.com/guide/topics/manifest/uses-feature-element.html#features-reference for
     * available system features.
     *
     * @param context               can be null, but will just return an empty list
     * @param labelSystemFeatureMap a map which has ui labels as key and android system feature string
     *                              (as returned as name by {@link PackageManager#getSystemAvailableFeatures()}) as value
     * @return list of page-entries (one for each map entry)
     */
    public static List<PageEntry<?>> createSystemFeatureInfo(@Nullable Context context, Map<CharSequence, String> labelSystemFeatureMap) {
        List<PageEntry<?>> entries = new ArrayList<>();
        if (context != null) {

            for (Map.Entry<CharSequence, String> entry : labelSystemFeatureMap.entrySet()) {
                boolean supported;
                if (entry.getValue().matches("^-?\\d+$")) {
                    final ConfigurationInfo configurationInfo = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getDeviceConfigurationInfo();
                    supported = configurationInfo.reqGlEsVersion >= Integer.valueOf(entry.getValue());
                } else {
                    supported = context.getPackageManager().hasSystemFeature(entry.getValue());
                }
                entries.add(Hood.get().createPropertyEntry(entry.getKey(), String.valueOf(supported)));
            }
        }
        return entries;
    }

    /**
     * Returns a list of the most important {@link TelephonyManager} states and ids. Will only
     * work if the app has the {@link Manifest.permission#READ_PHONE_STATE} permission or just
     * returns an empty list.
     *
     * @param context can be null, but will just return an empty list
     * @return the list of entries
     */
    //@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
    public static Section.HeaderSection createSectionTelephonyManger(@Nullable Context context) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Telephony Status");
        if (context != null) {
            if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
                section.setErrorMessage("Cannot display data - requires READ_PHONE_STATE permission.");
            } else {
                final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
                section.add(Hood.get().createPropertyEntry("sim-serial", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return telephonyManager.getSimSerialNumber();
                    }
                }));
                section.add(Hood.get().createPropertyEntry("sim-state", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return TypeTranslators.translateSimState(telephonyManager.getSimState());
                    }
                }));
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    section.add(Hood.get().createPropertyEntry("sim-slots", new DynamicValue<String>() {
                        @TargetApi(Build.VERSION_CODES.M)
                        @Override
                        public String getValue() {
                            return String.valueOf(telephonyManager.getPhoneCount());
                        }
                    }));
                }
                section.add(Hood.get().createPropertyEntry("subscriber-id", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return telephonyManager.getSubscriberId();
                    }
                }));
                section.add(Hood.get().createPropertyEntry("operator", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return telephonyManager.getNetworkOperatorName();
                    }
                }));
                section.add(Hood.get().createPropertyEntry("connection-type", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return TypeTranslators.translateTelephonyNetworkType(telephonyManager.getNetworkType());
                    }
                }));
                section.add(Hood.get().createPropertyEntry("MSISDN", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return telephonyManager.getLine1Number();
                    }
                }));
                section.add(Hood.get().createPropertyEntry("IMEI", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        return telephonyManager.getDeviceId();
                    }
                }));
            }
        }
        return section;
    }

    /**
     * Adds info for current {@link StrictMode} config. Currently only shows the bitmask used to
     * store the policies states. Unfortunately there is not much public API to examine StrictMode
     *
     * @return section
     */
    public static Section.HeaderSection createSectionStrictMode() {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("StrictMode");
        section.add(Hood.get().createPropertyEntry("thread-policy", new DynamicValue<String>() {
            @Override
            public String getValue() {
                return String.valueOf(StrictMode.getThreadPolicy());
            }
        }, true));
        section.add(Hood.get().createPropertyEntry("vm-policy", new DynamicValue<String>() {
            @Override
            public String getValue() {
                return StrictMode.getVmPolicy().toString();
            }
        }, true));
        return section;
    }

    public static Section.HeaderSection createSectionAndroidDebugSettings(final Context context) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Android Debug Settings");
        if (context != null) {
            section.add(Hood.get().createPropertyEntry("developer-mode", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    int settingsInt;
                    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) {
                        settingsInt = Settings.Global.getInt(context.getContentResolver(), Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0);
                    } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
                        settingsInt = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.DEVELOPMENT_SETTINGS_ENABLED, 0);
                    } else {
                        return "unknown (SDK 16+ only)";
                    }
                    return String.valueOf(settingsInt == 1);
                }
            }, DefaultButtonDefinitions.getDevSettingsAction().onClickAction, false));
            section.add(Hood.get().createPropertyEntry("usb-debugging", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    int settingsInt;
                    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) {
                        settingsInt = Settings.Global.getInt(context.getContentResolver(), Settings.Global.ADB_ENABLED, 0);
                    } else {
                        settingsInt = Settings.System.getInt(context.getContentResolver(), Settings.System.ADB_ENABLED, 0);
                    }
                    return String.valueOf(settingsInt == 1);
                }
            }, DefaultButtonDefinitions.getDevSettingsAction().onClickAction, false));
            section.add(Hood.get().createPropertyEntry("don't keep activities", new DynamicValue<String>() {
                @Override
                public String getValue() {
                    int settingsInt;
                    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) {
                        settingsInt = Settings.Global.getInt(context.getContentResolver(), Settings.Global.ALWAYS_FINISH_ACTIVITIES, 0);
                    } else {
                        settingsInt = Settings.System.getInt(context.getContentResolver(), Settings.System.ALWAYS_FINISH_ACTIVITIES, 0);
                    }
                    return String.valueOf(settingsInt == 1);
                }
            }, DefaultButtonDefinitions.getDevSettingsAction().onClickAction, false));
        }
        return section;
    }

    /**
     * Convenience to create a CI and source control section. Pass everything you want and keep the rest null.
     *
     * @param scmRev      git hash e.g. "git log -1 --format=%H"
     * @param scmBranch   git branch e.g. "git rev-parse --abbrev-ref HEAD"
     * @param ciBuildId   build number
     * @param ciBuildJob  job name
     * @param ciBuildTime time on ci server
     * @return the section containing all the data
     */
    public static Section.HeaderSection createSectionSourceControlAndCI(@Nullable String scmRev, @Nullable String scmBranch, @Nullable String scmCommitDate,
                                                                        @Nullable String ciBuildId, @Nullable String ciBuildJob, @Nullable String ciBuildTime) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Source Control & CI");
        if (scmBranch != null) {
            section.add(Hood.get().createPropertyEntry("scm-branch", scmBranch));
        }
        if (scmRev != null) {
            section.add(Hood.get().createPropertyEntry("scm-rev", scmRev));
        }
        if (scmCommitDate != null) {
            section.add(Hood.get().createPropertyEntry("scm-date", scmCommitDate));
        }
        if (ciBuildJob != null) {
            section.add(Hood.get().createPropertyEntry("ci-job", ciBuildJob));
        }
        if (ciBuildId != null) {
            section.add(Hood.get().createPropertyEntry("ci-build-id", ciBuildId));
        }
        if (ciBuildTime != null) {
            section.add(Hood.get().createPropertyEntry("ci-build-time", ciBuildTime));
        }
        return section;
    }

    /**
     * Creates a section with current battery status and health
     *
     * @param context can be null, but will just return an empty section
     * @return section
     */
    public static Section.HeaderSection createSectionBatteryInfo(@Nullable final Context context) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Battery Info");
        if (context != null) {
            final IntentFilter battIntentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
            final Intent batteryStatus = context.registerReceiver(null, battIntentFilter);
            if (batteryStatus != null) {
                section.add(Hood.get().createPropertyEntry("level", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        final Intent batteryStatus = context.registerReceiver(null, battIntentFilter);
                        float level = (float) batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                        float maxScale = (float) batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
                        float battPercent = level / maxScale * 100;
                        return battPercent + "%";
                    }
                }, DefaultButtonDefinitions.getBatterySummarySettingsAction().onClickAction, false));
                section.add(Hood.get().createPropertyEntry("status", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        final Intent batteryStatus = context.registerReceiver(null, battIntentFilter);
                        return TypeTranslators.translateBatteryStatus(batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1))
                                + " (" + TypeTranslators.translateBatteryPlugged(batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)) + ")";
                    }
                }));
                section.add(Hood.get().createPropertyEntry("health", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        final Intent batteryStatus = context.registerReceiver(null, battIntentFilter);
                        return TypeTranslators.translateBatteryHealth(batteryStatus.getIntExtra(BatteryManager.EXTRA_HEALTH, -1));
                    }
                }));
                section.add(Hood.get().createPropertyEntry("temp/volt",
                        new DynamicValue<String>() {
                            @Override
                            public String getValue() {
                                final Intent batteryStatus = context.registerReceiver(null, battIntentFilter);
                                return (float) batteryStatus.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1) / 10f + "°C/" +
                                        batteryStatus.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1) + "mV";
                            }
                        }
                ));
                section.add(Hood.get().createPropertyEntry("batt-saving", new DynamicValue<String>() {
                    @Override
                    public String getValue() {
                        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                            return String.valueOf(powerManager.isPowerSaveMode());
                        }
                        return "unsupported";
                    }
                }, DefaultButtonDefinitions.getBatterySaverSettingsAction().onClickAction, false));
            }
        }
        return section;
    }

    /**
     * Converts a {@link Properties} object to page-entries
     *
     * @param properties
     * @return list of page-entries
     */
    public static List<PageEntry<?>> createPropertiesEntries(@NonNull Properties properties) {
        return createFromMap(properties);
    }

    /**
     * Converts an arbitrary map to page-entries. Uses {@link #toString()} on the contained objects.
     *
     * @param hashMap
     * @return list of page-entries
     */
    public static List<PageEntry<?>> createFromMap(Map<?, ?> hashMap) {
        return createFromEntrySet(hashMap.entrySet());
    }

    private static List<PageEntry<?>> createFromEntrySet(Set<? extends Map.Entry<?, ?>> entrySet) {
        List<PageEntry<?>> entries = new ArrayList<>();
        for (Map.Entry propEntry : entrySet) {
            if (propEntry != null && propEntry.getKey() != null && propEntry.getValue() != null) {
                entries.add(Hood.get().createPropertyEntry(propEntry.getKey().toString(), propEntry.getValue().toString()));
            }
        }
        return entries;
    }

    /**
     * Creates a section showing some stats from {@link TrafficStats} showing tx/rx bytes and packages
     * of tagged sockets from boot until now.
     *
     * @param uids optional uid to get rx/tx bytes and packages
     * @return section
     */
    public static Section.HeaderSection createTransferStatSection(int... uids) {
        Section.ModifiableHeaderSection section = Hood.ext().createSection("Traffic Stats");

        if (uids != null) {
            for (int uid : uids) {
                section.add(createTxRxdSection("Socket " + uid, TrafficStats.getUidTxBytes(uid), TrafficStats.getUidTxPackets(uid),
                        TrafficStats.getUidRxBytes(uid), TrafficStats.getUidRxPackets(uid)));
            }
        }
        section.add(createTxRxdSection("Mobile", TrafficStats.getMobileTxBytes(), TrafficStats.getMobileTxPackets(),
                TrafficStats.getMobileRxBytes(), TrafficStats.getMobileRxPackets()));
        section.add(createTxRxdSection("Total", TrafficStats.getTotalTxBytes(), TrafficStats.getTotalTxPackets(),
                TrafficStats.getTotalRxBytes(), TrafficStats.getTotalRxPackets()));

        return section;
    }

    private static List<PageEntry<?>> createTxRxdSection(String name, final long txBytes,
                                                         final long txPackets, final long rxBytes, final long rxPackets) {
        List<PageEntry<?>> list = new LinkedList<>();
        list.add(Hood.get().createPropertyEntry(name + " TX", new DynamicValue<String>() {
            @Override
            public String getValue() {
                return txBytes == TrafficStats.UNSUPPORTED ? "UNSUPPORTED" : txPackets + " pkt / " + HoodUtil.humanReadableByteCount(txBytes, false);
            }
        }));
        list.add(Hood.get().createPropertyEntry(name + " RCVD", new DynamicValue<String>() {
            @Override
            public String getValue() {
                return rxPackets == TrafficStats.UNSUPPORTED ? "UNSUPPORTED" : rxPackets + " pkt / " + HoodUtil.humanReadableByteCount(rxBytes, false);
            }
        }));
        return list;
    }
}