diff options
| author | 2022-05-04 13:29:10 +0000 | |
|---|---|---|
| committer | 2022-05-04 13:29:10 +0000 | |
| commit | 4d3c2b3b3ee1331f5f6108bbbf7b4f442b11e457 (patch) | |
| tree | 326b5b8afb5246ce271e1d6e323b413ac195fd1d | |
| parent | 4c81f0577bb018571bef7d54e1abd5823bb6c344 (diff) | |
| parent | 7d58f8562f84b16adfb272213adbb00c6b9e3f1f (diff) | |
Merge changes I6a3f56bd,Ib5f39eb1 into tm-dev am: 1999fa1097 am: 7d58f8562f
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17888925
Change-Id: I27355b6f1a0a429edd2a0a90d23022a849e1ff48
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
5 files changed, 789 insertions, 240 deletions
diff --git a/core/java/android/os/logcat/ILogcatManagerService.aidl b/core/java/android/os/logcat/ILogcatManagerService.aidl index 29b4570ac71e..67a930a17665 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/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 217166c6810b..d32acaf1e620 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6776,9 +6776,8 @@ </activity> <activity android:name="com.android.server.logcat.LogAccessDialogActivity" - android:theme="@style/Theme.DeviceDefault.Dialog.Alert.DayNight" + android:theme="@style/Theme.Translucent.NoTitleBar" android:excludeFromRecents="true" - android:label="@string/log_access_confirmation_title" android:exported="false"> </activity> diff --git a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java index 79088d0398d2..f9a84077817d 100644 --- a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java +++ b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java @@ -16,26 +16,28 @@ package com.android.server.logcat; +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; -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; +import android.view.LayoutInflater; import android.view.View; 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 @@ -43,61 +45,61 @@ 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 final ILogcatManagerService mLogcatManagerService = - ILogcatManagerService.Stub.asInterface(ServiceManager.getService("logcat")); + private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000; + private static final int MSG_DISMISS_DIALOG = 0; - private String mPackageName; + 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; 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); + // retrieve Intent extra information + if (!readIntentInfo(getIntent())) { + Slog.e(TAG, "Invalid Intent extras, finishing"); + finish(); + return; + } - } 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 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 +111,26 @@ 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; + } + + if (!intent.hasExtra(Intent.EXTRA_UID)) { + Slog.e(TAG, "Missing EXTRA_UID"); + 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); + mUid = intent.getIntExtra(Intent.EXTRA_UID, 0); + + return true; } private Handler mHandler = new Handler() { @@ -133,11 +140,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 +151,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 +169,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) { @@ -202,21 +195,17 @@ 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: - 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() { + 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 5dccd071e250..21beb964529a 100644 --- a/services/core/java/com/android/server/logcat/LogcatManagerService.java +++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java @@ -16,103 +16,332 @@ 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.ComponentName; 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; /** * Service responsible for managing the access to Logcat. */ public final class LogcatManagerService extends SystemService { - private static final String TAG = "LogcatManagerService"; + 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 ILogd mLogdService; - private @NonNull ActivityManager mActivityManager; + private final LogcatManagerServiceInternal mLocalService; + private final Handler mHandler; 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 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()); } + } - @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); + 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 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(); + 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; + } } - return mLogdService; } } + 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); + } + /** * Returns the package name. * 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; } @@ -125,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 @@ -142,127 +371,164 @@ public final class LogcatManagerService extends SystemService { } return firstPackageName; - } - private void declineLogdAccess(int uid, int gid, int pid, int fd) { - try { - getLogdService().decline(uid, gid, pid, fd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); + 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 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 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 class LogdMonitor implements Runnable { + private void processNewLogAccessRequest(LogAccessClient client) { + boolean isInstrumented = mActivityManagerInternal.isUidCurrentlyInstrumented(client.mUid); - private final int mUid; - private final int mGid; - private final int mPid; - private final int mFd; - private final boolean mStart; + // The instrumented apks only run for testing, so we don't check user permission. + if (isInstrumented) { + onAccessApprovedForClient(client); + return; + } - /** - * 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; + if (!shouldShowConfirmationDialog(client)) { + onAccessDeclinedForClient(client); + return; } - /** - * 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(); - } + final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + logAccessStatus.mStatus = STATUS_PENDING; - if (mStart) { + mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_PENDING_TIMEOUT, client), + mClock.get() + PENDING_CONFIRMATION_TIMEOUT_MILLIS); + final Intent mIntent = createIntent(client); + mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM); + } - ActivityManagerInternal ami = LocalServices.getService( - ActivityManagerInternal.class); - boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(mUid); + void onAccessApprovedForClient(LogAccessClient client) { + scheduleStatusExpiry(client); - // 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; - } + LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus != null) { + for (LogAccessRequest request : logAccessStatus.mPendingRequests) { + approveRequest(client, request); + } + logAccessStatus.mStatus = STATUS_APPROVED; + logAccessStatus.mPendingRequests.clear(); + } + } - 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; - } - } + void onAccessDeclinedForClient(LogAccessClient client) { + scheduleStatusExpiry(client); - /** - * If the process is background or cannot retrieve the package name, - * decline the logd access. - **/ - declineLogdAccess(mUid, mGid, mPid, mFd); - return; + LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus != null) { + for (LogAccessRequest request : logAccessStatus.mPendingRequests) { + declineRequest(request); } + logAccessStatus.mStatus = STATUS_DECLINED; + logAccessStatus.mPendingRequests.clear(); } } - public LogcatManagerService(Context context) { - super(context); - mContext = context; - mBinderService = new BinderService(); - mThreadExecutor = Executors.newCachedThreadPool(); - mActivityManager = context.getSystemService(ActivityManager.class); + 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); } - @Override - public void onStart() { + 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 { - publishBinderService("logcat", mBinderService); - } catch (Throwable t) { - Slog.e(TAG, "Could not start the LogcatManagerService.", t); + 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 void addLogdService() { - mLogdService = ILogd.Stub.asInterface(ServiceManager.getService("logd")); + private void declineRequest(LogAccessRequest request) { + if (DEBUG) { + Slog.d(TAG, "Declining log access: " + request); + } + try { + getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd); + } catch (RemoteException e) { + Slog.e(TAG, "Fails to call remote functions", e); + } } /** * Create the Intent for LogAccessDialogActivity. */ - public Intent createIntent(String targetPackageName, int uid, int gid, int pid, int fd) { - final Intent intent = new Intent(); + 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.setComponent(new ComponentName(TARGET_PACKAGE_NAME, TARGET_ACTIVITY_NAME)); + 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 000000000000..f33001774263 --- /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<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(); + } +} |