/*
* Copyright (C) 2016. Jared Rummler <jared.rummler@gmail.com>
*
* 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 com.jaredrummler.android.processes;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import com.jaredrummler.android.processes.models.AndroidAppProcess;
import com.jaredrummler.android.processes.models.AndroidProcess;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* <p>Helper class to get a list of processes on Android.</p>
* <hr>
* <p><strong>Usage:</strong></p>
*
* <p>Get a list of running apps:</p>
* <pre>
* List<AndroidAppProcess> processes = AndroidProcesses.getRunningAppProcesses();
* </pre>
*
* <p>Get some information about a process:</p>
* <pre>
* AndroidAppProcess process = processes.get(location);
* String processName = process.name;
*
* Stat stat = process.stat();
* int pid = stat.getPid();
* int parentProcessId = stat.ppid();
* long startTime = stat.stime();
* int policy = stat.policy();
* char state = stat.state();
*
* Statm statm = process.statm();
* long totalSizeOfProcess = statm.getSize();
* long residentSetSize = statm.getResidentSetSize();
*
* PackageInfo packageInfo = process.getPackageInfo(context, 0);
* String appName = packageInfo.applicationInfo.loadLabel(pm).toString();
* </pre>
*
* <p>Check if your app is in the foreground:</p>
* <pre>
* if (AndroidProcesses.isMyProcessInTheForeground()) {
* // do stuff
* }
* </pre>
*
* <p>Get a list of application processes that are running on the device:</p>
* <pre>
* List<ActivityManager.RunningAppProcessInfo> processes = AndroidProcesses.getRunningAppProcessInfo(context);
* </pre>
*
* <hr>
* <p><strong>Limitations</strong></p>
*
* <p>System apps may not be visible because they have a higher SELinux context than third party apps.</p>
* <p>Some information that was available through {@link ActivityManager#getRunningAppProcesses()} is not available
* using this library
* ({@link RunningAppProcessInfo#pkgList},
* {@link RunningAppProcessInfo#lru},
* {@link RunningAppProcessInfo#importance},
* etc.).</p>
* <p>This is currently not working on the N developer preview.</p>
* <hr>
* <p><b>Note:</b> You should avoid running methods from this class on the UI thread.</p>
*/
public class AndroidProcesses {
public static final String TAG = "AndroidProcesses";
private static final int AID_READPROC = 3009;
private static boolean loggingEnabled;
/**
* Toggle whether debug logging is enabled.
*
* @param enabled
* {@code true} to enable logging. This should be only be used for debugging purposes.
* @see #isLoggingEnabled()
* @see #log(String, Object...)
* @see #log(Throwable, String, Object...)
*/
public static void setLoggingEnabled(boolean enabled) {
loggingEnabled = enabled;
}
/**
* @return {@code true} if logging is enabled.
* @see #setLoggingEnabled(boolean)
*/
public static boolean isLoggingEnabled() {
return loggingEnabled;
}
/**
* Send a log message if logging is enabled.
*
* @param message
* the message to log
* @param args
* list of arguments to pass to the formatter
*/
public static void log(String message, Object... args) {
if (loggingEnabled) {
Log.d(TAG, args.length == 0 ? message : String.format(message, args));
}
}
/**
* Send a log message if logging is enabled.
*
* @param error
* An exception to log
* @param message
* the message to log
* @param args
* list of arguments to pass to the formatter
*/
public static void log(Throwable error, String message, Object... args) {
if (loggingEnabled) {
Log.d(TAG, args.length == 0 ? message : String.format(message, args), error);
}
}
/**
* On Android 7.0+ the procfs filesystem is now mounted with hidepid=2, eliminating access to the /proc/PID
* directories of other users. There's a group ("readproc") for making exceptions but it's not exposed as a
* permission. To get a list of processes on Android 7.0+ you must use {@link android.app.usage.UsageStatsManager}
* or have root access.
*
* @return {@code true} if procfs is mounted with hidepid=2
*/
public static boolean isProcessInfoHidden() {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("/proc/mounts"));
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
String[] columns = line.split("\\s+");
if (columns.length == 6 && columns[1].equals("/proc")) {
return columns[3].contains("hidepid=1") || columns[3].contains("hidepid=2");
}
}
} catch (IOException e) {
Log.d(TAG, "Error reading /proc/mounts. Checking if UID 'readproc' exists.");
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {
}
}
}
return android.os.Process.getUidForName("readproc") == AID_READPROC;
}
/**
* @return a list of <i>all</i> processes running on the device.
*/
public static List<AndroidProcess> getRunningProcesses() {
List<AndroidProcess> processes = new ArrayList<>();
File[] files = new File("/proc").listFiles();
for (File file : files) {
if (file.isDirectory()) {
int pid;
try {
pid = Integer.parseInt(file.getName());
} catch (NumberFormatException e) {
continue;
}
try {
processes.add(new AndroidProcess(pid));
} catch (IOException e) {
log(e, "Error reading from /proc/%d.", pid);
// System apps will not be readable on Android 5.0+ if SELinux is enforcing.
// You will need root access or an elevated SELinux context to read all files under /proc.
}
}
}
return processes;
}
/**
* @return a list of all running app processes on the device.
*/
public static List<AndroidAppProcess> getRunningAppProcesses() {
List<AndroidAppProcess> processes = new ArrayList<>();
File[] files = new File("/proc").listFiles();
for (File file : files) {
if (file.isDirectory()) {
int pid;
try {
pid = Integer.parseInt(file.getName());
} catch (NumberFormatException e) {
continue;
}
try {
processes.add(new AndroidAppProcess(pid));
} catch (AndroidAppProcess.NotAndroidAppProcessException ignored) {
} catch (IOException e) {
log(e, "Error reading from /proc/%d.", pid);
// System apps will not be readable on Android 5.0+ if SELinux is enforcing.
// You will need root access or an elevated SELinux context to read all files under /proc.
}
}
}
return processes;
}
/**
* Get a list of user apps running in the foreground.
*
* @param context
* the application context
* @return a list of user apps that are in the foreground.
*/
public static List<AndroidAppProcess> getRunningForegroundApps(Context context) {
List<AndroidAppProcess> processes = new ArrayList<>();
File[] files = new File("/proc").listFiles();
PackageManager pm = context.getPackageManager();
for (File file : files) {
if (file.isDirectory()) {
int pid;
try {
pid = Integer.parseInt(file.getName());
} catch (NumberFormatException e) {
continue;
}
try {
AndroidAppProcess process = new AndroidAppProcess(pid);
if (process.foreground
// ignore system processes. First app user starts at 10000.
&& (process.uid < 1000 || process.uid > 9999)
// ignore processes that are not running in the default app process.
&& !process.name.contains(":")
// Ignore processes that the user cannot launch.
&& pm.getLaunchIntentForPackage(process.getPackageName()) != null) {
processes.add(process);
}
} catch (AndroidAppProcess.NotAndroidAppProcessException ignored) {
} catch (IOException e) {
log(e, "Error reading from /proc/%d.", pid);
// System apps will not be readable on Android 5.0+ if SELinux is enforcing.
// You will need root access or an elevated SELinux context to read all files under /proc.
}
}
}
return processes;
}
/**
* @return {@code true} if this process is in the foreground.
*/
public static boolean isMyProcessInTheForeground() {
try {
return new AndroidAppProcess(android.os.Process.myPid()).foreground;
} catch (Exception e) {
log(e, "Error finding our own process");
}
return false;
}
/**
* Returns a list of application processes that are running on the device.
*
* <p><b>NOTE:</b> On Lollipop (SDK 22) this does not provide
* {@link RunningAppProcessInfo#pkgList},
* {@link RunningAppProcessInfo#importance},
* {@link RunningAppProcessInfo#lru},
* {@link RunningAppProcessInfo#importanceReasonCode},
* {@link RunningAppProcessInfo#importanceReasonComponent},
* {@link RunningAppProcessInfo#importanceReasonPid},
* etc. If you need more process information try using
* {@link #getRunningAppProcesses()} or {@link android.app.usage.UsageStatsManager}</p>
*
* @param context
* the application context
* @return a list of RunningAppProcessInfo records, or null if there are no
* running processes (it will not return an empty list). This list ordering is not
* specified.
*/
public static List<RunningAppProcessInfo> getRunningAppProcessInfo(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
List<AndroidAppProcess> runningAppProcesses = AndroidProcesses.getRunningAppProcesses();
List<RunningAppProcessInfo> appProcessInfos = new ArrayList<>();
for (AndroidAppProcess process : runningAppProcesses) {
RunningAppProcessInfo info = new RunningAppProcessInfo(process.name, process.pid, null);
info.uid = process.uid;
// TODO: Get more information about the process. pkgList, importance, lru, etc.
appProcessInfos.add(info);
}
return appProcessInfos;
}
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
return am.getRunningAppProcesses();
}
/* package */ AndroidProcesses() {
throw new AssertionError("no instances");
}
/**
* A {@link Comparator} to list processes by name
*/
public static final class ProcessComparator implements Comparator<AndroidProcess> {
@Override public int compare(AndroidProcess p1, AndroidProcess p2) {
return p1.name.compareToIgnoreCase(p2.name);
}
}
}