1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base;
6 
7 import android.annotation.TargetApi;
8 import android.app.ActivityManager;
9 import android.content.Context;
10 import android.content.pm.PackageManager;
11 import android.os.Build;
12 import android.os.Environment;
13 import android.os.StatFs;
14 import android.os.StrictMode;
15 import android.util.Log;
16 
17 import androidx.annotation.VisibleForTesting;
18 
19 import org.chromium.base.annotations.CalledByNative;
20 import org.chromium.base.annotations.JNINamespace;
21 import org.chromium.base.annotations.MainDex;
22 import org.chromium.base.annotations.NativeMethods;
23 import org.chromium.base.metrics.RecordHistogram;
24 
25 import java.io.BufferedReader;
26 import java.io.FileReader;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 
30 /**
31  * Exposes system related information about the current device.
32  */
33 @JNINamespace("base::android")
34 @MainDex
35 public class SysUtils {
36     // A device reporting strictly more total memory in megabytes cannot be considered 'low-end'.
37     private static final int ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB = 512;
38     private static final int ANDROID_O_LOW_MEMORY_DEVICE_THRESHOLD_MB = 1024;
39     private static final int BYTES_PER_GIGABYTE = 1024 * 1024 * 1024;
40 
41     // A device reporting more disk capacity in gigabytes than this is considered high end.
42     private static final long HIGH_END_DEVICE_DISK_CAPACITY_GB = 24;
43 
44     private static final String TAG = "SysUtils";
45 
46     private static Boolean sLowEndDevice;
47     private static Integer sAmountOfPhysicalMemoryKB;
48 
49     private static Boolean sHighEndDiskDevice;
50 
SysUtils()51     private SysUtils() { }
52 
53     /**
54      * Return the amount of physical memory on this device in kilobytes.
55      * @return Amount of physical memory in kilobytes, or 0 if there was
56      *         an error trying to access the information.
57      */
detectAmountOfPhysicalMemoryKB()58     private static int detectAmountOfPhysicalMemoryKB() {
59         // Extract total memory RAM size by parsing /proc/meminfo, note that
60         // this is exactly what the implementation of sysconf(_SC_PHYS_PAGES)
61         // does. However, it can't be called because this method must be
62         // usable before any native code is loaded.
63 
64         // An alternative is to use ActivityManager.getMemoryInfo(), but this
65         // requires a valid ActivityManager handle, which can only come from
66         // a valid Context object, which itself cannot be retrieved
67         // during early startup, where this method is called. And making it
68         // an explicit parameter here makes all call paths _much_ more
69         // complicated.
70 
71         Pattern pattern = Pattern.compile("^MemTotal:\\s+([0-9]+) kB$");
72         // Synchronously reading files in /proc in the UI thread is safe.
73         StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
74         try {
75             FileReader fileReader = new FileReader("/proc/meminfo");
76             try {
77                 BufferedReader reader = new BufferedReader(fileReader);
78                 try {
79                     String line;
80                     for (;;) {
81                         line = reader.readLine();
82                         if (line == null) {
83                             Log.w(TAG, "/proc/meminfo lacks a MemTotal entry?");
84                             break;
85                         }
86                         Matcher m = pattern.matcher(line);
87                         if (!m.find()) continue;
88 
89                         int totalMemoryKB = Integer.parseInt(m.group(1));
90                         // Sanity check.
91                         if (totalMemoryKB <= 1024) {
92                             Log.w(TAG, "Invalid /proc/meminfo total size in kB: " + m.group(1));
93                             break;
94                         }
95 
96                         return totalMemoryKB;
97                     }
98 
99                 } finally {
100                     reader.close();
101                 }
102             } finally {
103                 fileReader.close();
104             }
105         } catch (Exception e) {
106             Log.w(TAG, "Cannot get total physical size from /proc/meminfo", e);
107         } finally {
108             StrictMode.setThreadPolicy(oldPolicy);
109         }
110 
111         return 0;
112     }
113 
114     /**
115      * @return Whether or not this device should be considered a low end device.
116      */
117     @CalledByNative
isLowEndDevice()118     public static boolean isLowEndDevice() {
119         if (sLowEndDevice == null) {
120             sLowEndDevice = detectLowEndDevice();
121         }
122         return sLowEndDevice.booleanValue();
123     }
124 
125     /**
126      * @return Whether or not this device should be considered a low end device.
127      */
amountOfPhysicalMemoryKB()128     public static int amountOfPhysicalMemoryKB() {
129         if (sAmountOfPhysicalMemoryKB == null) {
130             sAmountOfPhysicalMemoryKB = detectAmountOfPhysicalMemoryKB();
131         }
132         return sAmountOfPhysicalMemoryKB.intValue();
133     }
134 
135     /**
136      * @return Whether or not the system has low available memory.
137      */
138     @CalledByNative
isCurrentlyLowMemory()139     public static boolean isCurrentlyLowMemory() {
140         ActivityManager am =
141                 (ActivityManager) ContextUtils.getApplicationContext().getSystemService(
142                         Context.ACTIVITY_SERVICE);
143         ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
144         am.getMemoryInfo(info);
145         return info.lowMemory;
146     }
147 
148     /**
149      * Resets the cached value, if any.
150      */
151     @VisibleForTesting
resetForTesting()152     public static void resetForTesting() {
153         sLowEndDevice = null;
154         sAmountOfPhysicalMemoryKB = null;
155     }
156 
hasCamera(final Context context)157     public static boolean hasCamera(final Context context) {
158         final PackageManager pm = context.getPackageManager();
159         // JellyBean support.
160         boolean hasCamera = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);
161         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
162             hasCamera |= pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
163         }
164         return hasCamera;
165     }
166 
167     @TargetApi(Build.VERSION_CODES.KITKAT)
detectLowEndDevice()168     private static boolean detectLowEndDevice() {
169         assert CommandLine.isInitialized();
170         if (CommandLine.getInstance().hasSwitch(BaseSwitches.ENABLE_LOW_END_DEVICE_MODE)) {
171             return true;
172         }
173         if (CommandLine.getInstance().hasSwitch(BaseSwitches.DISABLE_LOW_END_DEVICE_MODE)) {
174             return false;
175         }
176 
177         // If this logic changes, update the comments above base::SysUtils::IsLowEndDevice.
178         sAmountOfPhysicalMemoryKB = detectAmountOfPhysicalMemoryKB();
179         boolean isLowEnd = true;
180         if (sAmountOfPhysicalMemoryKB <= 0) {
181             isLowEnd = false;
182         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
183             isLowEnd = sAmountOfPhysicalMemoryKB / 1024 <= ANDROID_O_LOW_MEMORY_DEVICE_THRESHOLD_MB;
184         } else {
185             isLowEnd = sAmountOfPhysicalMemoryKB / 1024 <= ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB;
186         }
187 
188         // For evaluation purposes check whether our computation agrees with Android API value.
189         Context appContext = ContextUtils.getApplicationContext();
190         boolean isLowRam = false;
191         if (appContext != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
192             isLowRam = ((ActivityManager) ContextUtils.getApplicationContext().getSystemService(
193                                 Context.ACTIVITY_SERVICE))
194                                .isLowRamDevice();
195         }
196         RecordHistogram.recordBooleanHistogram(
197                 "Android.SysUtilsLowEndMatches", isLowEnd == isLowRam);
198 
199         return isLowEnd;
200     }
201 
202     /**
203      * Creates a new trace event to log the number of minor / major page faults, if tracing is
204      * enabled.
205      */
logPageFaultCountToTracing()206     public static void logPageFaultCountToTracing() {
207         SysUtilsJni.get().logPageFaultCountToTracing();
208     }
209 
210     /**
211      * @return Whether or not this device should be considered a high end device from a disk
212      *         capacity point of view.
213      */
isHighEndDiskDevice()214     public static boolean isHighEndDiskDevice() {
215         if (sHighEndDiskDevice == null) {
216             sHighEndDiskDevice = detectHighEndDiskDevice();
217         }
218         return sHighEndDiskDevice.booleanValue();
219     }
220 
detectHighEndDiskDevice()221     private static boolean detectHighEndDiskDevice() {
222         try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
223             StatFs dataStats = new StatFs(Environment.getDataDirectory().getAbsolutePath());
224             long totalGBytes = dataStats.getTotalBytes() / BYTES_PER_GIGABYTE;
225             return totalGBytes >= HIGH_END_DEVICE_DISK_CAPACITY_GB;
226         } catch (IllegalArgumentException e) {
227             Log.v(TAG, "Cannot get disk data capacity", e);
228         }
229         return false;
230     }
231 
232     @VisibleForTesting
setAmountOfPhysicalMemoryKBForTesting(int physicalMemoryKB)233     public static void setAmountOfPhysicalMemoryKBForTesting(int physicalMemoryKB) {
234         sAmountOfPhysicalMemoryKB = physicalMemoryKB;
235     }
236 
237     /**
238      * @return Whether this device is running Android Go. This is assumed when we're running Android
239      * O or later and we're on a low-end device.
240      */
isAndroidGo()241     public static boolean isAndroidGo() {
242         return isLowEndDevice() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
243     }
244 
245     @NativeMethods
246     interface Natives {
logPageFaultCountToTracing()247         void logPageFaultCountToTracing();
248     }
249 }
250