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
This commit is contained in:
Robert Horvath
2022-04-21 17:52:46 +02:00
parent 2b6a23400c
commit d112201586
4 changed files with 744 additions and 203 deletions

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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<Long> 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<LogAccessRequest> mPendingRequests = new ArrayList<>();
}
private final Map<LogAccessClient, LogAccessStatus> mLogAccessStatus = new ArrayMap<>();
private final Map<LogAccessClient, Integer> 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<Long> 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;
}

View File

@@ -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<Long> 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 <T> void addLocalServiceMock(Class<T> 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();
}
}