// Copyright 2013 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.base; import android.app.ActivityManager; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.os.StrictMode; import android.util.Log; import org.jni_zero.CalledByNative; import org.jni_zero.JNINamespace; import org.jni_zero.NativeMethods; import org.chromium.build.BuildConfig; import java.io.BufferedReader; import java.io.FileReader; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Exposes system related information about the current device. */ @JNINamespace("base::android") public class SysUtils { // A device reporting strictly more total memory in megabytes cannot be considered 'low-end'. private static final int ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB = 512; private static final int ANDROID_O_LOW_MEMORY_DEVICE_THRESHOLD_MB = 1024; private static final int BYTES_PER_GIGABYTE = 1024 * 1024 * 1024; // A device reporting more disk capacity in gigabytes than this is considered high end. private static final long HIGH_END_DEVICE_DISK_CAPACITY_GB = 24; private static final String TAG = "SysUtils"; private static Boolean sLowEndDevice; private static Integer sAmountOfPhysicalMemoryKB; private static Boolean sHighEndDiskDevice; private SysUtils() {} /** * Return the amount of physical memory on this device in kilobytes. * @return Amount of physical memory in kilobytes, or 0 if there was * an error trying to access the information. */ private static int detectAmountOfPhysicalMemoryKB() { // Extract total memory RAM size by parsing /proc/meminfo, note that // this is exactly what the implementation of sysconf(_SC_PHYS_PAGES) // does. However, it can't be called because this method must be // usable before any native code is loaded. // An alternative is to use ActivityManager.getMemoryInfo(), but this // requires a valid ActivityManager handle, which can only come from // a valid Context object, which itself cannot be retrieved // during early startup, where this method is called. And making it // an explicit parameter here makes all call paths _much_ more // complicated. Pattern pattern = Pattern.compile("^MemTotal:\\s+([0-9]+) kB$"); // Synchronously reading files in /proc in the UI thread is safe. StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); try { FileReader fileReader = new FileReader("/proc/meminfo"); try { BufferedReader reader = new BufferedReader(fileReader); try { String line; for (; ; ) { line = reader.readLine(); if (line == null) { Log.w(TAG, "/proc/meminfo lacks a MemTotal entry?"); break; } Matcher m = pattern.matcher(line); if (!m.find()) continue; int totalMemoryKB = Integer.parseInt(m.group(1)); // Sanity check. if (totalMemoryKB <= 1024) { Log.w(TAG, "Invalid /proc/meminfo total size in kB: " + m.group(1)); break; } return totalMemoryKB; } } finally { reader.close(); } } finally { fileReader.close(); } } catch (Exception e) { Log.w(TAG, "Cannot get total physical size from /proc/meminfo", e); } finally { StrictMode.setThreadPolicy(oldPolicy); } return 0; } /** * @return Whether or not this device should be considered a low end device. */ @CalledByNative public static boolean isLowEndDevice() { // Do not cache in tests since command-line flags can change. if (sLowEndDevice == null || BuildConfig.IS_FOR_TEST) { sLowEndDevice = detectLowEndDevice(); } return sLowEndDevice.booleanValue(); } /** * @return amount of physical ram detected in KB, or 0 if detection failed. */ @CalledByNative public static int amountOfPhysicalMemoryKB() { if (sAmountOfPhysicalMemoryKB == null) { sAmountOfPhysicalMemoryKB = detectAmountOfPhysicalMemoryKB(); } return sAmountOfPhysicalMemoryKB.intValue(); } /** * @return Whether or not the system has low available memory. */ @CalledByNative public static boolean isCurrentlyLowMemory() { ActivityManager am = (ActivityManager) ContextUtils.getApplicationContext() .getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo(); try { am.getMemoryInfo(info); return info.lowMemory; } catch (SecurityException e) { // Occurs on Redmi devices when called from isolated processes. // https://crbug.com/1480655 return false; } } public static boolean hasCamera(final Context context) { final PackageManager pm = context.getPackageManager(); return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); } private static boolean detectLowEndDevice() { assert CommandLine.isInitialized(); if (CommandLine.getInstance().hasSwitch(BaseSwitches.ENABLE_LOW_END_DEVICE_MODE)) { return true; } if (CommandLine.getInstance().hasSwitch(BaseSwitches.DISABLE_LOW_END_DEVICE_MODE)) { return false; } // If this logic changes, update the comments above base::SysUtils::IsLowEndDevice. int physicalMemoryKb = amountOfPhysicalMemoryKB(); boolean isLowEnd = true; if (physicalMemoryKb <= 0) { isLowEnd = false; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { isLowEnd = physicalMemoryKb / 1024 <= ANDROID_O_LOW_MEMORY_DEVICE_THRESHOLD_MB; } else { isLowEnd = physicalMemoryKb / 1024 <= ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB; } return isLowEnd; } /** * Creates a new trace event to log the number of minor / major page faults, if tracing is * enabled. */ public static void logPageFaultCountToTracing() { SysUtilsJni.get().logPageFaultCountToTracing(); } /** * @return Whether or not this device should be considered a high end device from a disk * capacity point of view. */ public static boolean isHighEndDiskDevice() { if (sHighEndDiskDevice == null) { sHighEndDiskDevice = detectHighEndDiskDevice(); } return sHighEndDiskDevice.booleanValue(); } private static boolean detectHighEndDiskDevice() { try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { StatFs dataStats = new StatFs(Environment.getDataDirectory().getAbsolutePath()); long totalGBytes = dataStats.getTotalBytes() / BYTES_PER_GIGABYTE; return totalGBytes >= HIGH_END_DEVICE_DISK_CAPACITY_GB; } catch (IllegalArgumentException e) { Log.v(TAG, "Cannot get disk data capacity", e); } return false; } /** * @return Whether this device is running Android Go. This is assumed when we're running Android * O or later and we're on a low-end device. */ public static boolean isAndroidGo() { return isLowEndDevice() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } @NativeMethods interface Natives { void logPageFaultCountToTracing(); } }