From 2b6a23400cca111c5bb05e5a58eef30c87b72764 Mon Sep 17 00:00:00 2001 From: Robert Horvath Date: Tue, 19 Apr 2022 11:12:14 +0200 Subject: [PATCH 1/2] LogAccessDialogActivity improvements 1. Makes the activity under AlertDialog translucent When the AlertDialog is dismissed, a dialog like view can be seen with the activity's label (with a "%s" placeholder) can be seen. This change makes the activity itself translucent. 2. Handles cancel/dismiss of the dialog When the dialog is canceled (eg. pressing back button or tapping outside the dialog) decline the log access request. When the dialog is dismissed finish the activity hosting the dialog. 3. Clean up unused code, some refactoring, more error handling Bug: 228001277 Bug: 228563090 Test: Manual Change-Id: Ib5f39eb12b41ed4137bc281b12e30072c9f5b7c7 --- core/res/AndroidManifest.xml | 3 +- .../logcat/LogAccessDialogActivity.java | 161 ++++++++++-------- .../server/logcat/LogcatManagerService.java | 28 +-- 3 files changed, 100 insertions(+), 92 deletions(-) diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 217166c6810b5..d32acaf1e6204 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6776,9 +6776,8 @@ diff --git a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java index 79088d0398d21..e0ecadf3229e9 100644 --- a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java +++ b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java @@ -16,12 +16,19 @@ package com.android.server.logcat; +import static com.android.server.logcat.LogcatManagerService.EXTRA_FD; +import static com.android.server.logcat.LogcatManagerService.EXTRA_GID; +import static com.android.server.logcat.LogcatManagerService.EXTRA_PID; +import static com.android.server.logcat.LogcatManagerService.EXTRA_UID; + +import android.annotation.StyleRes; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -30,7 +37,9 @@ import android.os.ServiceManager; import android.os.UserHandle; import android.os.logcat.ILogcatManagerService; import android.util.Slog; +import android.view.ContextThemeWrapper; import android.view.InflateException; +import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.TextView; @@ -43,61 +52,64 @@ import com.android.internal.R; public class LogAccessDialogActivity extends Activity implements View.OnClickListener { private static final String TAG = LogAccessDialogActivity.class.getSimpleName(); - private Context mContext; + + private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000; + private static final int MSG_DISMISS_DIALOG = 0; private final ILogcatManagerService mLogcatManagerService = ILogcatManagerService.Stub.asInterface(ServiceManager.getService("logcat")); private String mPackageName; - private int mUid; private int mGid; private int mPid; private int mFd; + private String mAlertTitle; private AlertDialog.Builder mAlertDialog; private AlertDialog mAlert; private View mAlertView; - private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000; - private static final int MSG_DISMISS_DIALOG = 0; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - try { - mContext = this; - - // retrieve Intent extra information - Intent intent = getIntent(); - getIntentInfo(intent); - - // retrieve the title string from passed intent extra - mAlertTitle = getTitleString(mContext, mPackageName, mUid); - - // creaet View - mAlertView = createView(); - - // create AlertDialog - mAlertDialog = new AlertDialog.Builder(this); - mAlertDialog.setView(mAlertView); - - // show Alert - mAlert = mAlertDialog.create(); - mAlert.show(); - - // set Alert Timeout - mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT); - - } catch (Exception e) { - try { - Slog.e(TAG, "onCreate failed, declining the logd access", e); - mLogcatManagerService.decline(mUid, mGid, mPid, mFd); - } catch (RemoteException ex) { - Slog.e(TAG, "Fails to call remote functions", ex); - } + // retrieve Intent extra information + if (!readIntentInfo(getIntent())) { + Slog.e(TAG, "Invalid Intent extras, finishing"); + finish(); + return; } + + // retrieve the title string from passed intent extra + try { + mAlertTitle = getTitleString(this, mPackageName, mUid); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Unable to fetch label of package " + mPackageName, e); + declineLogAccess(); + finish(); + return; + } + + // create View + boolean isDarkTheme = (getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + int themeId = isDarkTheme ? android.R.style.Theme_DeviceDefault_Dialog_Alert : + android.R.style.Theme_DeviceDefault_Light_Dialog_Alert; + mAlertView = createView(themeId); + + // create AlertDialog + mAlertDialog = new AlertDialog.Builder(this, themeId); + mAlertDialog.setView(mAlertView); + mAlertDialog.setOnCancelListener(dialog -> declineLogAccess()); + mAlertDialog.setOnDismissListener(dialog -> finish()); + + // show Alert + mAlert = mAlertDialog.create(); + mAlert.show(); + + // set Alert Timeout + mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT); } @Override @@ -109,21 +121,44 @@ public class LogAccessDialogActivity extends Activity implements mAlert = null; } - private void getIntentInfo(Intent intent) throws Exception { - + private boolean readIntentInfo(Intent intent) { if (intent == null) { - throw new NullPointerException("Intent is null"); + Slog.e(TAG, "Intent is null"); + return false; } mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); if (mPackageName == null || mPackageName.length() == 0) { - throw new NullPointerException("Package Name is null"); + Slog.e(TAG, "Missing package name extra"); + return false; } - mUid = intent.getIntExtra("com.android.server.logcat.uid", 0); - mGid = intent.getIntExtra("com.android.server.logcat.gid", 0); - mPid = intent.getIntExtra("com.android.server.logcat.pid", 0); - mFd = intent.getIntExtra("com.android.server.logcat.fd", 0); + if (!intent.hasExtra(EXTRA_UID)) { + Slog.e(TAG, "Missing EXTRA_UID"); + return false; + } + + if (!intent.hasExtra(EXTRA_GID)) { + Slog.e(TAG, "Missing EXTRA_GID"); + return false; + } + + if (!intent.hasExtra(EXTRA_PID)) { + Slog.e(TAG, "Missing EXTRA_PID"); + return false; + } + + if (!intent.hasExtra(EXTRA_FD)) { + Slog.e(TAG, "Missing EXTRA_FD"); + return false; + } + + mUid = intent.getIntExtra(EXTRA_UID, 0); + mGid = intent.getIntExtra(EXTRA_GID, 0); + mPid = intent.getIntExtra(EXTRA_PID, 0); + mFd = intent.getIntExtra(EXTRA_FD, 0); + + return true; } private Handler mHandler = new Handler() { @@ -133,11 +168,7 @@ public class LogAccessDialogActivity extends Activity implements if (mAlert != null) { mAlert.dismiss(); mAlert = null; - try { - mLogcatManagerService.decline(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } + declineLogAccess(); } break; @@ -148,25 +179,15 @@ public class LogAccessDialogActivity extends Activity implements }; private String getTitleString(Context context, String callingPackage, int uid) - throws Exception { - + throws NameNotFoundException { PackageManager pm = context.getPackageManager(); - if (pm == null) { - throw new NullPointerException("PackageManager is null"); - } CharSequence appLabel = pm.getApplicationInfoAsUser(callingPackage, PackageManager.MATCH_DIRECT_BOOT_AUTO, UserHandle.getUserId(uid)).loadLabel(pm); - if (appLabel == null || appLabel.length() == 0) { - throw new NameNotFoundException("Application Label is null"); - } String titleString = context.getString( com.android.internal.R.string.log_access_confirmation_title, appLabel); - if (titleString == null || titleString.length() == 0) { - throw new NullPointerException("Title is null"); - } return titleString; } @@ -176,9 +197,9 @@ public class LogAccessDialogActivity extends Activity implements * If we cannot retrieve the package name, it returns null and we decline the full device log * access */ - private View createView() throws Exception { - - final View view = getLayoutInflater().inflate( + private View createView(@StyleRes int themeId) { + Context themedContext = new ContextThemeWrapper(getApplicationContext(), themeId); + final View view = LayoutInflater.from(themedContext).inflate( R.layout.log_access_user_consent_dialog_permission, null /*root*/); if (view == null) { @@ -210,13 +231,17 @@ public class LogAccessDialogActivity extends Activity implements finish(); break; case R.id.log_access_dialog_deny_button: - try { - mLogcatManagerService.decline(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } + declineLogAccess(); finish(); break; } } + + private void declineLogAccess() { + try { + mLogcatManagerService.decline(mUid, mGid, mPid, mFd); + } catch (RemoteException e) { + Slog.e(TAG, "Fails to call remote functions", e); + } + } } diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java index 5dccd071e250b..f4d2b6e69fccb 100644 --- a/services/core/java/com/android/server/logcat/LogcatManagerService.java +++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java @@ -16,10 +16,8 @@ package com.android.server.logcat; -import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManagerInternal; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -42,22 +40,16 @@ import java.util.concurrent.Executors; * Service responsible for managing the access to Logcat. */ public final class LogcatManagerService extends SystemService { - private static final String TAG = "LogcatManagerService"; + static final String EXTRA_UID = "com.android.server.logcat.uid"; + static final String EXTRA_GID = "com.android.server.logcat.gid"; + static final String EXTRA_PID = "com.android.server.logcat.pid"; + static final String EXTRA_FD = "com.android.server.logcat.fd"; + private final Context mContext; private final BinderService mBinderService; private final ExecutorService mThreadExecutor; private ILogd mLogdService; - private @NonNull ActivityManager mActivityManager; - private ActivityManagerInternal mActivityManagerInternal; - private static final int MAX_UID_IMPORTANCE_COUNT_LISTENER = 2; - private static final String TARGET_PACKAGE_NAME = "android"; - private static final String TARGET_ACTIVITY_NAME = - "com.android.server.logcat.LogAccessDialogActivity"; - private static final String EXTRA_UID = "com.android.server.logcat.uid"; - private static final String EXTRA_GID = "com.android.server.logcat.gid"; - private static final String EXTRA_PID = "com.android.server.logcat.pid"; - private static final String EXTRA_FD = "com.android.server.logcat.fd"; private final class BinderService extends ILogcatManagerService.Stub { @Override @@ -153,11 +145,6 @@ public final class LogcatManagerService extends SystemService { } } - private static String getClientInfo(int uid, int gid, int pid, int fd) { - return "UID=" + Integer.toString(uid) + " GID=" + Integer.toString(gid) + " PID=" - + Integer.toString(pid) + " FD=" + Integer.toString(fd); - } - private class LogdMonitor implements Runnable { private final int mUid; @@ -232,7 +219,6 @@ public final class LogcatManagerService extends SystemService { mContext = context; mBinderService = new BinderService(); mThreadExecutor = Executors.newCachedThreadPool(); - mActivityManager = context.getSystemService(ActivityManager.class); } @Override @@ -252,7 +238,7 @@ public final class LogcatManagerService extends SystemService { * Create the Intent for LogAccessDialogActivity. */ public Intent createIntent(String targetPackageName, int uid, int gid, int pid, int fd) { - final Intent intent = new Intent(); + final Intent intent = new Intent(mContext, LogAccessDialogActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -262,8 +248,6 @@ public final class LogcatManagerService extends SystemService { intent.putExtra(EXTRA_PID, pid); intent.putExtra(EXTRA_FD, fd); - intent.setComponent(new ComponentName(TARGET_PACKAGE_NAME, TARGET_ACTIVITY_NAME)); - return intent; } } From d1122015868a4473632467c4b5e1fc70b3a15605 Mon Sep 17 00:00:00 2001 From: Robert Horvath Date: Thu, 21 Apr 2022 17:52:46 +0200 Subject: [PATCH 2/2] Batch log access requests from same client For multiple log access requests from the same client (same UID + package name) within a short timeframe, show only one confirmation prompt to the user. When access has been approved/denied, further requests will automatically be approved/denied until another timeout expires, after which a new request will show a prompt again. If the prompt is shown but the request isn't approved or denied within a certain time, the client will automatically be denied access. Moved the approve/decline methods out of ILogcatManagerService into a local service, so that they can only be called from within the system server. Bug: 229976778 Test: atest FrameworksServicesTests:LogcatManagerServiceTest Change-Id: I6a3f56bdcbb84e64b1b24e73476bd24f32b75f24 --- .../os/logcat/ILogcatManagerService.aidl | 27 - .../logcat/LogAccessDialogActivity.java | 50 +- .../server/logcat/LogcatManagerService.java | 548 +++++++++++++----- .../logcat/LogcatManagerServiceTest.java | 322 ++++++++++ 4 files changed, 744 insertions(+), 203 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java diff --git a/core/java/android/os/logcat/ILogcatManagerService.aidl b/core/java/android/os/logcat/ILogcatManagerService.aidl index 29b4570ac71ed..67a930a176653 100644 --- a/core/java/android/os/logcat/ILogcatManagerService.aidl +++ b/core/java/android/os/logcat/ILogcatManagerService.aidl @@ -42,31 +42,4 @@ oneway interface ILogcatManagerService { * @param fd The FD (Socket) of client who makes the request. */ void finishThread(in int uid, in int gid, in int pid, in int fd); - - - /** - * The function is called by UX component to notify - * LogcatManagerService that the user approved - * the privileged log data access. - * - * @param uid The UID of client who makes the request. - * @param gid The GID of client who makes the request. - * @param pid The PID of client who makes the request. - * @param fd The FD (Socket) of client who makes the request. - */ - void approve(in int uid, in int gid, in int pid, in int fd); - - - /** - * The function is called by UX component to notify - * LogcatManagerService that the user declined - * the privileged log data access. - * - * @param uid The UID of client who makes the request. - * @param gid The GID of client who makes the request. - * @param pid The PID of client who makes the request. - * @param fd The FD (Socket) of client who makes the request. - */ - void decline(in int uid, in int gid, in int pid, in int fd); } - diff --git a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java index e0ecadf3229e9..f9a84077817d9 100644 --- a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java +++ b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java @@ -16,11 +16,6 @@ package com.android.server.logcat; -import static com.android.server.logcat.LogcatManagerService.EXTRA_FD; -import static com.android.server.logcat.LogcatManagerService.EXTRA_GID; -import static com.android.server.logcat.LogcatManagerService.EXTRA_PID; -import static com.android.server.logcat.LogcatManagerService.EXTRA_UID; - import android.annotation.StyleRes; import android.app.Activity; import android.app.AlertDialog; @@ -32,10 +27,7 @@ import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; -import android.os.logcat.ILogcatManagerService; import android.util.Slog; import android.view.ContextThemeWrapper; import android.view.InflateException; @@ -45,6 +37,7 @@ import android.widget.Button; import android.widget.TextView; import com.android.internal.R; +import com.android.server.LocalServices; /** * Dialog responsible for obtaining user consent per-use log access @@ -56,14 +49,11 @@ public class LogAccessDialogActivity extends Activity implements private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000; private static final int MSG_DISMISS_DIALOG = 0; - private final ILogcatManagerService mLogcatManagerService = - ILogcatManagerService.Stub.asInterface(ServiceManager.getService("logcat")); + private final LogcatManagerService.LogcatManagerServiceInternal mLogcatManagerInternal = + LocalServices.getService(LogcatManagerService.LogcatManagerServiceInternal.class); private String mPackageName; private int mUid; - private int mGid; - private int mPid; - private int mFd; private String mAlertTitle; private AlertDialog.Builder mAlertDialog; @@ -133,30 +123,12 @@ public class LogAccessDialogActivity extends Activity implements return false; } - if (!intent.hasExtra(EXTRA_UID)) { + if (!intent.hasExtra(Intent.EXTRA_UID)) { Slog.e(TAG, "Missing EXTRA_UID"); return false; } - if (!intent.hasExtra(EXTRA_GID)) { - Slog.e(TAG, "Missing EXTRA_GID"); - return false; - } - - if (!intent.hasExtra(EXTRA_PID)) { - Slog.e(TAG, "Missing EXTRA_PID"); - return false; - } - - if (!intent.hasExtra(EXTRA_FD)) { - Slog.e(TAG, "Missing EXTRA_FD"); - return false; - } - - mUid = intent.getIntExtra(EXTRA_UID, 0); - mGid = intent.getIntExtra(EXTRA_GID, 0); - mPid = intent.getIntExtra(EXTRA_PID, 0); - mFd = intent.getIntExtra(EXTRA_FD, 0); + mUid = intent.getIntExtra(Intent.EXTRA_UID, 0); return true; } @@ -223,11 +195,7 @@ public class LogAccessDialogActivity extends Activity implements public void onClick(View view) { switch (view.getId()) { case R.id.log_access_dialog_allow_button: - try { - mLogcatManagerService.approve(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } + mLogcatManagerInternal.approveAccessForClient(mUid, mPackageName); finish(); break; case R.id.log_access_dialog_deny_button: @@ -238,10 +206,6 @@ public class LogAccessDialogActivity extends Activity implements } private void declineLogAccess() { - try { - mLogcatManagerService.decline(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } + mLogcatManagerInternal.declineAccessForClient(mUid, mPackageName); } } diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java index f4d2b6e69fccb..21beb964529a0 100644 --- a/services/core/java/com/android/server/logcat/LogcatManagerService.java +++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java @@ -16,24 +16,39 @@ package com.android.server.logcat; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Handler; import android.os.ILogd; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserHandle; import android.os.logcat.ILogcatManagerService; +import android.util.ArrayMap; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.SystemService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; /** @@ -41,57 +56,282 @@ import java.util.concurrent.Executors; */ public final class LogcatManagerService extends SystemService { private static final String TAG = "LogcatManagerService"; - static final String EXTRA_UID = "com.android.server.logcat.uid"; - static final String EXTRA_GID = "com.android.server.logcat.gid"; - static final String EXTRA_PID = "com.android.server.logcat.pid"; - static final String EXTRA_FD = "com.android.server.logcat.fd"; + private static final boolean DEBUG = false; + + /** How long to wait for the user to approve/decline before declining automatically */ + @VisibleForTesting + static final int PENDING_CONFIRMATION_TIMEOUT_MILLIS = Build.IS_DEBUGGABLE ? 70000 : 400000; + + /** + * How long an approved / declined status is valid for. + * + * After a client has been approved/declined log access, if they try to access logs again within + * this timeout, the new request will be automatically approved/declined. + * Only after this timeout expires will a new request generate another prompt to the user. + **/ + @VisibleForTesting + static final int STATUS_EXPIRATION_TIMEOUT_MILLIS = 60 * 1000; + + private static final int MSG_LOG_ACCESS_REQUESTED = 0; + private static final int MSG_APPROVE_LOG_ACCESS = 1; + private static final int MSG_DECLINE_LOG_ACCESS = 2; + private static final int MSG_LOG_ACCESS_FINISHED = 3; + private static final int MSG_PENDING_TIMEOUT = 4; + private static final int MSG_LOG_ACCESS_STATUS_EXPIRED = 5; + + private static final int STATUS_NEW_REQUEST = 0; + private static final int STATUS_PENDING = 1; + private static final int STATUS_APPROVED = 2; + private static final int STATUS_DECLINED = 3; + + @IntDef(prefix = {"STATUS_"}, value = { + STATUS_NEW_REQUEST, + STATUS_PENDING, + STATUS_APPROVED, + STATUS_DECLINED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface LogAccessRequestStatus { + } private final Context mContext; + private final Injector mInjector; + private final Supplier mClock; private final BinderService mBinderService; - private final ExecutorService mThreadExecutor; + private final LogcatManagerServiceInternal mLocalService; + private final Handler mHandler; + private ActivityManagerInternal mActivityManagerInternal; private ILogd mLogdService; + private static final class LogAccessClient { + final int mUid; + @NonNull + final String mPackageName; + + LogAccessClient(int uid, @NonNull String packageName) { + mUid = uid; + mPackageName = packageName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LogAccessClient)) return false; + LogAccessClient that = (LogAccessClient) o; + return mUid == that.mUid && Objects.equals(mPackageName, that.mPackageName); + } + + @Override + public int hashCode() { + return Objects.hash(mUid, mPackageName); + } + + @Override + public String toString() { + return "LogAccessClient{" + + "mUid=" + mUid + + ", mPackageName=" + mPackageName + + '}'; + } + } + + private static final class LogAccessRequest { + final int mUid; + final int mGid; + final int mPid; + final int mFd; + + private LogAccessRequest(int uid, int gid, int pid, int fd) { + mUid = uid; + mGid = gid; + mPid = pid; + mFd = fd; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LogAccessRequest)) return false; + LogAccessRequest that = (LogAccessRequest) o; + return mUid == that.mUid && mGid == that.mGid && mPid == that.mPid && mFd == that.mFd; + } + + @Override + public int hashCode() { + return Objects.hash(mUid, mGid, mPid, mFd); + } + + @Override + public String toString() { + return "LogAccessRequest{" + + "mUid=" + mUid + + ", mGid=" + mGid + + ", mPid=" + mPid + + ", mFd=" + mFd + + '}'; + } + } + + private static final class LogAccessStatus { + @LogAccessRequestStatus + int mStatus = STATUS_NEW_REQUEST; + final List mPendingRequests = new ArrayList<>(); + } + + private final Map mLogAccessStatus = new ArrayMap<>(); + private final Map mActiveLogAccessCount = new ArrayMap<>(); + private final class BinderService extends ILogcatManagerService.Stub { @Override public void startThread(int uid, int gid, int pid, int fd) { - mThreadExecutor.execute(new LogdMonitor(uid, gid, pid, fd, true)); + final LogAccessRequest logAccessRequest = new LogAccessRequest(uid, gid, pid, fd); + if (DEBUG) { + Slog.d(TAG, "New log access request: " + logAccessRequest); + } + final Message msg = mHandler.obtainMessage(MSG_LOG_ACCESS_REQUESTED, logAccessRequest); + mHandler.sendMessageAtTime(msg, mClock.get()); } @Override public void finishThread(int uid, int gid, int pid, int fd) { - // TODO This thread will be used to notify the AppOpsManager that - // the logd data access is finished. - mThreadExecutor.execute(new LogdMonitor(uid, gid, pid, fd, false)); + final LogAccessRequest logAccessRequest = new LogAccessRequest(uid, gid, pid, fd); + if (DEBUG) { + Slog.d(TAG, "Log access finished: " + logAccessRequest); + } + final Message msg = mHandler.obtainMessage(MSG_LOG_ACCESS_FINISHED, logAccessRequest); + mHandler.sendMessageAtTime(msg, mClock.get()); + } + } + + final class LogcatManagerServiceInternal { + public void approveAccessForClient(int uid, @NonNull String packageName) { + final LogAccessClient client = new LogAccessClient(uid, packageName); + if (DEBUG) { + Slog.d(TAG, "Approving log access for client: " + client); + } + final Message msg = mHandler.obtainMessage(MSG_APPROVE_LOG_ACCESS, client); + mHandler.sendMessageAtTime(msg, mClock.get()); } - @Override - public void approve(int uid, int gid, int pid, int fd) { - try { - Slog.d(TAG, "Allow logd access for uid: " + uid); - getLogdService().approve(uid, gid, pid, fd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } - } - - @Override - public void decline(int uid, int gid, int pid, int fd) { - try { - Slog.d(TAG, "Decline logd access for uid: " + uid); - getLogdService().decline(uid, gid, pid, fd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); + public void declineAccessForClient(int uid, @NonNull String packageName) { + final LogAccessClient client = new LogAccessClient(uid, packageName); + if (DEBUG) { + Slog.d(TAG, "Declining log access for client: " + client); } + final Message msg = mHandler.obtainMessage(MSG_DECLINE_LOG_ACCESS, client); + mHandler.sendMessageAtTime(msg, mClock.get()); } } private ILogd getLogdService() { - synchronized (LogcatManagerService.this) { - if (mLogdService == null) { - LogcatManagerService.this.addLogdService(); - } - return mLogdService; + if (mLogdService == null) { + mLogdService = mInjector.getLogdService(); } + return mLogdService; + } + + private static class LogAccessRequestHandler extends Handler { + private final LogcatManagerService mService; + + LogAccessRequestHandler(Looper looper, LogcatManagerService service) { + super(looper); + mService = service; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOG_ACCESS_REQUESTED: { + LogAccessRequest request = (LogAccessRequest) msg.obj; + mService.onLogAccessRequested(request); + break; + } + case MSG_APPROVE_LOG_ACCESS: { + LogAccessClient client = (LogAccessClient) msg.obj; + mService.onAccessApprovedForClient(client); + break; + } + case MSG_DECLINE_LOG_ACCESS: { + LogAccessClient client = (LogAccessClient) msg.obj; + mService.onAccessDeclinedForClient(client); + break; + } + case MSG_LOG_ACCESS_FINISHED: { + LogAccessRequest request = (LogAccessRequest) msg.obj; + mService.onLogAccessFinished(request); + break; + } + case MSG_PENDING_TIMEOUT: { + LogAccessClient client = (LogAccessClient) msg.obj; + mService.onPendingTimeoutExpired(client); + break; + } + case MSG_LOG_ACCESS_STATUS_EXPIRED: { + LogAccessClient client = (LogAccessClient) msg.obj; + mService.onAccessStatusExpired(client); + break; + } + } + } + } + + static class Injector { + protected Supplier createClock() { + return SystemClock::uptimeMillis; + } + + protected Looper getLooper() { + return Looper.getMainLooper(); + } + + protected ILogd getLogdService() { + return ILogd.Stub.asInterface(ServiceManager.getService("logd")); + } + } + + public LogcatManagerService(Context context) { + this(context, new Injector()); + } + + public LogcatManagerService(Context context, Injector injector) { + super(context); + mContext = context; + mInjector = injector; + mClock = injector.createClock(); + mBinderService = new BinderService(); + mLocalService = new LogcatManagerServiceInternal(); + mHandler = new LogAccessRequestHandler(injector.getLooper(), this); + } + + @Override + public void onStart() { + try { + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + publishBinderService("logcat", mBinderService); + publishLocalService(LogcatManagerServiceInternal.class, mLocalService); + } catch (Throwable t) { + Slog.e(TAG, "Could not start the LogcatManagerService.", t); + } + } + + @VisibleForTesting + LogcatManagerServiceInternal getLocalService() { + return mLocalService; + } + + @VisibleForTesting + ILogcatManagerService getBinderService() { + return mBinderService; + } + + @Nullable + private LogAccessClient getClientForRequest(LogAccessRequest request) { + final String packageName = getPackageName(request); + if (packageName == null) { + return null; + } + + return new LogAccessClient(request.mUid, packageName); } /** @@ -99,12 +339,9 @@ public final class LogcatManagerService extends SystemService { * If we cannot retrieve the package name, it returns null and we decline the full device log * access */ - private String getPackageName(int uid, int gid, int pid, int fd) { - - final ActivityManagerInternal activityManagerInternal = - LocalServices.getService(ActivityManagerInternal.class); - if (activityManagerInternal != null) { - String packageName = activityManagerInternal.getPackageNameByPid(pid); + private String getPackageName(LogAccessRequest request) { + if (mActivityManagerInternal != null) { + String packageName = mActivityManagerInternal.getPackageNameByPid(request.mPid); if (packageName != null) { return packageName; } @@ -117,7 +354,7 @@ public final class LogcatManagerService extends SystemService { return null; } - String[] packageNames = pm.getPackagesForUid(uid); + String[] packageNames = pm.getPackagesForUid(request.mUid); if (ArrayUtils.isEmpty(packageNames)) { // Decline the logd access if the app name is unknown @@ -134,119 +371,164 @@ public final class LogcatManagerService extends SystemService { } return firstPackageName; - } - private void declineLogdAccess(int uid, int gid, int pid, int fd) { + void onLogAccessRequested(LogAccessRequest request) { + final LogAccessClient client = getClientForRequest(request); + if (client == null) { + declineRequest(request); + return; + } + + LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus == null) { + logAccessStatus = new LogAccessStatus(); + mLogAccessStatus.put(client, logAccessStatus); + } + + switch (logAccessStatus.mStatus) { + case STATUS_NEW_REQUEST: + logAccessStatus.mPendingRequests.add(request); + processNewLogAccessRequest(client); + break; + case STATUS_PENDING: + logAccessStatus.mPendingRequests.add(request); + return; + case STATUS_APPROVED: + approveRequest(client, request); + break; + case STATUS_DECLINED: + declineRequest(request); + break; + } + } + + private boolean shouldShowConfirmationDialog(LogAccessClient client) { + // If the process is foreground, show a dialog for user consent + final int procState = mActivityManagerInternal.getUidProcessState(client.mUid); + return procState == ActivityManager.PROCESS_STATE_TOP; + } + + private void processNewLogAccessRequest(LogAccessClient client) { + boolean isInstrumented = mActivityManagerInternal.isUidCurrentlyInstrumented(client.mUid); + + // The instrumented apks only run for testing, so we don't check user permission. + if (isInstrumented) { + onAccessApprovedForClient(client); + return; + } + + if (!shouldShowConfirmationDialog(client)) { + onAccessDeclinedForClient(client); + return; + } + + final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + logAccessStatus.mStatus = STATUS_PENDING; + + mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_PENDING_TIMEOUT, client), + mClock.get() + PENDING_CONFIRMATION_TIMEOUT_MILLIS); + final Intent mIntent = createIntent(client); + mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM); + } + + void onAccessApprovedForClient(LogAccessClient client) { + scheduleStatusExpiry(client); + + LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus != null) { + for (LogAccessRequest request : logAccessStatus.mPendingRequests) { + approveRequest(client, request); + } + logAccessStatus.mStatus = STATUS_APPROVED; + logAccessStatus.mPendingRequests.clear(); + } + } + + void onAccessDeclinedForClient(LogAccessClient client) { + scheduleStatusExpiry(client); + + LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus != null) { + for (LogAccessRequest request : logAccessStatus.mPendingRequests) { + declineRequest(request); + } + logAccessStatus.mStatus = STATUS_DECLINED; + logAccessStatus.mPendingRequests.clear(); + } + } + + private void scheduleStatusExpiry(LogAccessClient client) { + mHandler.removeMessages(MSG_PENDING_TIMEOUT, client); + mHandler.removeMessages(MSG_LOG_ACCESS_STATUS_EXPIRED, client); + mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_LOG_ACCESS_STATUS_EXPIRED, client), + mClock.get() + STATUS_EXPIRATION_TIMEOUT_MILLIS); + } + + void onPendingTimeoutExpired(LogAccessClient client) { + final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus != null && logAccessStatus.mStatus == STATUS_PENDING) { + onAccessDeclinedForClient(client); + } + } + + void onAccessStatusExpired(LogAccessClient client) { + if (DEBUG) { + Slog.d(TAG, "Log access status expired for " + client); + } + mLogAccessStatus.remove(client); + } + + void onLogAccessFinished(LogAccessRequest request) { + final LogAccessClient client = getClientForRequest(request); + final int activeCount = mActiveLogAccessCount.getOrDefault(client, 1) - 1; + + if (activeCount == 0) { + mActiveLogAccessCount.remove(client); + if (DEBUG) { + Slog.d(TAG, "Client is no longer accessing logs: " + client); + } + // TODO This will be used to notify the AppOpsManager that the logd data access + // is finished. + } else { + mActiveLogAccessCount.put(client, activeCount); + } + } + + private void approveRequest(LogAccessClient client, LogAccessRequest request) { + if (DEBUG) { + Slog.d(TAG, "Approving log access: " + request); + } try { - getLogdService().decline(uid, gid, pid, fd); + getLogdService().approve(request.mUid, request.mGid, request.mPid, request.mFd); + Integer activeCount = mActiveLogAccessCount.getOrDefault(client, 0); + mActiveLogAccessCount.put(client, activeCount + 1); } catch (RemoteException e) { Slog.e(TAG, "Fails to call remote functions", e); } } - private class LogdMonitor implements Runnable { - - private final int mUid; - private final int mGid; - private final int mPid; - private final int mFd; - private final boolean mStart; - - /** - * For starting a thread, the start value is true. - * For finishing a thread, the start value is false. - */ - LogdMonitor(int uid, int gid, int pid, int fd, boolean start) { - mUid = uid; - mGid = gid; - mPid = pid; - mFd = fd; - mStart = start; + private void declineRequest(LogAccessRequest request) { + if (DEBUG) { + Slog.d(TAG, "Declining log access: " + request); } - - /** - * LogdMonitor generates a prompt for users. - * The users decide whether the logd access is allowed. - */ - @Override - public void run() { - if (mLogdService == null) { - LogcatManagerService.this.addLogdService(); - } - - if (mStart) { - - ActivityManagerInternal ami = LocalServices.getService( - ActivityManagerInternal.class); - boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(mUid); - - // The instrumented apks only run for testing, so we don't check user permission. - if (isCallerInstrumented) { - try { - getLogdService().approve(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } - return; - } - - final int procState = LocalServices.getService(ActivityManagerInternal.class) - .getUidProcessState(mUid); - // If the process is foreground and we can retrieve the package name, show a dialog - // for user consent - if (procState == ActivityManager.PROCESS_STATE_TOP) { - String packageName = getPackageName(mUid, mGid, mPid, mFd); - if (packageName != null) { - final Intent mIntent = createIntent(packageName, mUid, mGid, mPid, mFd); - mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM); - return; - } - } - - /** - * If the process is background or cannot retrieve the package name, - * decline the logd access. - **/ - declineLogdAccess(mUid, mGid, mPid, mFd); - return; - } - } - } - - public LogcatManagerService(Context context) { - super(context); - mContext = context; - mBinderService = new BinderService(); - mThreadExecutor = Executors.newCachedThreadPool(); - } - - @Override - public void onStart() { try { - publishBinderService("logcat", mBinderService); - } catch (Throwable t) { - Slog.e(TAG, "Could not start the LogcatManagerService.", t); + getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd); + } catch (RemoteException e) { + Slog.e(TAG, "Fails to call remote functions", e); } } - private void addLogdService() { - mLogdService = ILogd.Stub.asInterface(ServiceManager.getService("logd")); - } - /** * Create the Intent for LogAccessDialogActivity. */ - public Intent createIntent(String targetPackageName, int uid, int gid, int pid, int fd) { + public Intent createIntent(LogAccessClient client) { final Intent intent = new Intent(mContext, LogAccessDialogActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPackageName); - intent.putExtra(EXTRA_UID, uid); - intent.putExtra(EXTRA_GID, gid); - intent.putExtra(EXTRA_PID, pid); - intent.putExtra(EXTRA_FD, fd); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, client.mPackageName); + intent.putExtra(Intent.EXTRA_UID, client.mUid); return intent; } diff --git a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java new file mode 100644 index 0000000000000..f33001774263e --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2022 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 com.android.server.logcat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.content.ContextWrapper; +import android.os.ILogd; +import android.os.Looper; +import android.os.UserHandle; +import android.os.test.TestLooper; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.server.LocalServices; +import com.android.server.logcat.LogcatManagerService.Injector; +import com.android.server.testutils.OffsettableClock; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.Supplier; + +/** + * Tests for {@link com.android.server.logcat.LogcatManagerService}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:LogcatManagerServiceTest + */ +@SuppressWarnings("GuardedBy") +public class LogcatManagerServiceTest { + private static final String APP1_PACKAGE_NAME = "app1"; + private static final int APP1_UID = 10001; + private static final int APP1_GID = 10001; + private static final int APP1_PID = 10001; + private static final String APP2_PACKAGE_NAME = "app2"; + private static final int APP2_UID = 10002; + private static final int APP2_GID = 10002; + private static final int APP2_PID = 10002; + private static final int FD1 = 10; + private static final int FD2 = 11; + + @Mock + private ActivityManagerInternal mActivityManagerInternalMock; + @Mock + private ILogd mLogdMock; + + private LogcatManagerService mService; + private LogcatManagerService.LogcatManagerServiceInternal mLocalService; + private ContextWrapper mContextSpy; + private OffsettableClock mClock; + private TestLooper mTestLooper; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock); + mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + mClock = new OffsettableClock.Stopped(); + mTestLooper = new TestLooper(mClock::now); + + when(mActivityManagerInternalMock.getPackageNameByPid(APP1_PID)).thenReturn( + APP1_PACKAGE_NAME); + when(mActivityManagerInternalMock.getPackageNameByPid(APP2_PID)).thenReturn( + APP2_PACKAGE_NAME); + + mService = new LogcatManagerService(mContextSpy, new Injector() { + @Override + protected Supplier createClock() { + return mClock::now; + } + + @Override + protected Looper getLooper() { + return mTestLooper.getLooper(); + } + + @Override + protected ILogd getLogdService() { + return mLogdMock; + } + }); + mLocalService = mService.getLocalService(); + mService.onStart(); + } + + @After + public void tearDown() throws Exception { + LocalServices.removeServiceForTest(ActivityManagerInternal.class); + } + + /** + * Creates a mock and registers it to {@link LocalServices}. + */ + private static void addLocalServiceMock(Class clazz, T mock) { + LocalServices.removeServiceForTest(clazz); + LocalServices.addService(clazz, mock); + } + + @Test + public void test_RequestFromBackground_DeclinedWithoutPrompt() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_RECEIVER); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mContextSpy, never()).startActivityAsUser(any(), any()); + } + + @Test + public void test_RequestFromForegroundService_DeclinedWithoutPrompt() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mContextSpy, never()).startActivityAsUser(any(), any()); + } + + @Test + public void test_RequestFromTop_ShowsPrompt() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + } + + @Test + public void test_RequestFromTop_NoInteractionWithPrompt_DeclinesAfterTimeout() + throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + advanceTime(LogcatManagerService.PENDING_CONFIRMATION_TIMEOUT_MILLIS); + + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + } + + @Test + public void test_RequestFromTop_Approved() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + + mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + } + + @Test + public void test_RequestFromTop_Declined() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + + mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + } + + @Test + public void test_RequestFromTop_MultipleRequestsApprovedTogether() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2); + mTestLooper.dispatchAll(); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt()); + verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt()); + + mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD2); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD2); + } + + @Test + public void test_RequestFromTop_MultipleRequestsDeclinedTogether() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2); + mTestLooper.dispatchAll(); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt()); + verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt()); + + mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD2); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD2); + } + + @Test + public void test_RequestFromTop_Approved_DoesNotShowPromptAgain() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2); + mTestLooper.dispatchAll(); + + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD2); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD2); + } + + @Test + public void test_RequestFromTop_Declined_DoesNotShowPromptAgain() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2); + mTestLooper.dispatchAll(); + + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD2); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD2); + } + + @Test + public void test_RequestFromTop_Approved_ShowsPromptForDifferentClient() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + when(mActivityManagerInternalMock.getUidProcessState(APP2_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + mService.getBinderService().startThread(APP2_UID, APP2_GID, APP2_PID, FD2); + mTestLooper.dispatchAll(); + + verify(mContextSpy, times(2)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, never()).decline(APP2_UID, APP2_GID, APP2_PID, FD2); + verify(mLogdMock, never()).approve(APP2_UID, APP2_GID, APP2_PID, FD2); + } + + @Test + public void test_RequestFromTop_Approved_ShowPromptAgainAfterTimeout() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + advanceTime(LogcatManagerService.STATUS_EXPIRATION_TIMEOUT_MILLIS); + + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + verify(mContextSpy, times(2)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + } + + private void advanceTime(long timeMs) { + mClock.fastForward(timeMs); + mTestLooper.dispatchAll(); + } +}