ProcessUtil.java

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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 androidx.test.internal.util;

import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/** Util class to help extract and manipulate Android processes */
public final class ProcessUtil {
  private static final String TAG = "ProcessUtil";

  private static final List<Integer> RETRY_WAIT_INTERVALS =
      Collections.unmodifiableList(Arrays.asList(8, 8, 16, 32, 64, 128, 256));

  private static String processName;

  /**
   * Helper method to get the name of the process this instance is running in.
   *
   * @param context the context this process is running in.
   * @return returns the current process name or an empty sting if one cannot be found.
   */
  public static String getCurrentProcessName(Context context) {
    if (!TextUtils.isEmpty(processName)) {
      return processName;
    }

    try {
      processName = getCurrentProcessNameUsingActivityManager(context);
    } catch (SecurityException isIsolatedProcess) {
      Log.i(TAG, "Could not read process name from ActivityManager (isolatedProcess?)");
      // processName is uninitialized at this point, and the proc-based fallback will fail, since
      // we're in an isolated process and are forbidden from accessing the filesystem.
      // Terminate early with an empty process name.
      return "";
    }

    if (processName.isEmpty()) {
      Log.w(
          TAG,
          "Could not figure out process name using ActivityManager, falling back to use /proc. "
              + "Note that processName fetched from /proc may be truncated!");
      processName = getCurrentProcessNameUsingProc();
      if (processName.isEmpty()) {
        Log.w(TAG, "Could not figure out process name /proc either");
      }
    }
    return processName;
  }

  /**
   * Helper method to get the name of the process this instance is running in using the {@link
   * ActivityManager#getRunningAppProcesses}.
   *
   * <p>{@link ActivityManager#getRunningAppProcesses} method unfortunately was only designed for
   * aiding debuggin and it is known in some circunstancies to not return the current process in the
   * list (see http://b/152836562 for an example).
   *
   * @param context the context this process is running in.
   * @return returns the current process name or an empty sting if one cannot be found.
   */
  @VisibleForTesting
  static String getCurrentProcessNameUsingActivityManager(Context context) {
    int pid = android.os.Process.myPid();
    ActivityManager activityManager =
        (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

    if (activityManager != null) {
      List<RunningAppProcessInfo> runningAppProcesses = activityManager.getRunningAppProcesses();
      if (null == runningAppProcesses) {
        int retryAttempt = 0;
        // Retry getting running app processes
        while (null == runningAppProcesses && retryAttempt < RETRY_WAIT_INTERVALS.size()) {
          try {
            Log.i(TAG, "Waiting for running app processes...");
            Thread.sleep(RETRY_WAIT_INTERVALS.get(retryAttempt++));
            runningAppProcesses = activityManager.getRunningAppProcesses();
          } catch (InterruptedException ie) {
            Log.w(TAG, "Interrupted while waiting for running app processes", ie);
            return "";
          }
        }
      }

      for (ActivityManager.RunningAppProcessInfo processInfo : emptyIfNull(runningAppProcesses)) {
        if (processInfo.pid == pid) {
          return processInfo.processName;
        }
      }
      Log.w(TAG, "Couldn't get running processes from ActivityManager!");
    } else {
      Log.w(
          TAG,
          "ActivityManager#getRunningAppProcesses did not return an entry matching pid = " + pid);
    }
    return "";
  }

  private static <E> Iterable<E> emptyIfNull(Iterable<E> iterable) {
    return null == iterable ? Collections.emptyList() : iterable;
  }

  /**
   * Helper method to get the name of the process this instance is running in using information
   * published by the kernel in the /proc process.
   *
   * <p>This method is unfortunately only reliable if the current process name is relatively short.
   * We have empircally seen process names truncated at 75 chars but it could be even less.
   *
   * @return returns the current process name or an empty sting if one cannot be found.
   */
  @VisibleForTesting
  static String getCurrentProcessNameUsingProc() {
    BufferedReader br = null;
    String processName = "";
    try {
      br = new BufferedReader(new FileReader("/proc/self/cmdline"));
      processName = br.readLine().trim();
    } catch (IOException e) {
      Log.e(TAG, e.getMessage(), e);
    } finally {
      try {
        if (br != null) {
          br.close();
        }
      } catch (Exception e) {
        Log.w(TAG, e.getMessage(), e);
      }
    }
    return processName;
  }

  /** Only call this method to reset state in tests! */
  @VisibleForTesting
  static void resetProcessName() {
    processName = "";
  }
}