Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit aaca4bc5 authored by Robert Horvath's avatar Robert Horvath Committed by Automerger Merge Worker
Browse files

Merge changes I6a3f56bd,Ib5f39eb1 into tm-dev am: 1999fa10 am: 7d58f856

parents 6c6e7528 7d58f856
Loading
Loading
Loading
Loading
+0 −27
Original line number Original line Diff line number Diff line
@@ -42,31 +42,4 @@ oneway interface ILogcatManagerService {
     * @param fd  The FD (Socket) of client who makes the request.
     * @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);
    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);
}
}
+1 −2
Original line number Original line Diff line number Diff line
@@ -6776,9 +6776,8 @@
        </activity>
        </activity>


        <activity android:name="com.android.server.logcat.LogAccessDialogActivity"
        <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:excludeFromRecents="true"
                  android:label="@string/log_access_confirmation_title"
                  android:exported="false">
                  android:exported="false">
        </activity>
        </activity>


+69 −80
Original line number Original line Diff line number Diff line
@@ -16,26 +16,28 @@


package com.android.server.logcat;
package com.android.server.logcat;


import android.annotation.StyleRes;
import android.app.Activity;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Build;
import android.os.Bundle;
import android.os.Bundle;
import android.os.Handler;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserHandle;
import android.os.logcat.ILogcatManagerService;
import android.util.Slog;
import android.util.Slog;
import android.view.ContextThemeWrapper;
import android.view.InflateException;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View;
import android.widget.Button;
import android.widget.Button;
import android.widget.TextView;
import android.widget.TextView;


import com.android.internal.R;
import com.android.internal.R;
import com.android.server.LocalServices;


/**
/**
 * Dialog responsible for obtaining user consent per-use log access
 * Dialog responsible for obtaining user consent per-use log access
@@ -43,45 +45,54 @@ import com.android.internal.R;
public class LogAccessDialogActivity extends Activity implements
public class LogAccessDialogActivity extends Activity implements
        View.OnClickListener {
        View.OnClickListener {
    private static final String TAG = LogAccessDialogActivity.class.getSimpleName();
    private static final String TAG = LogAccessDialogActivity.class.getSimpleName();
    private Context mContext;


    private final ILogcatManagerService mLogcatManagerService =
    private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
            ILogcatManagerService.Stub.asInterface(ServiceManager.getService("logcat"));
    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 mUid;
    private int mGid;

    private int mPid;
    private int mFd;
    private String mAlertTitle;
    private String mAlertTitle;
    private AlertDialog.Builder mAlertDialog;
    private AlertDialog.Builder mAlertDialog;
    private AlertDialog mAlert;
    private AlertDialog mAlert;
    private View mAlertView;
    private View mAlertView;


    private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
    private static final int MSG_DISMISS_DIALOG = 0;

    @Override
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);


        try {
            mContext = this;

        // retrieve Intent extra information
        // retrieve Intent extra information
            Intent intent = getIntent();
        if (!readIntentInfo(getIntent())) {
            getIntentInfo(intent);
            Slog.e(TAG, "Invalid Intent extras, finishing");
            finish();
            return;
        }


        // retrieve the title string from passed intent extra
        // retrieve the title string from passed intent extra
            mAlertTitle = getTitleString(mContext, mPackageName, mUid);
        try {
            mAlertTitle = getTitleString(this, mPackageName, mUid);
        } catch (NameNotFoundException e) {
            Slog.e(TAG, "Unable to fetch label of package " + mPackageName, e);
            declineLogAccess();
            finish();
            return;
        }


            // creaet View
        // create View
            mAlertView = createView();
        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
        // create AlertDialog
            mAlertDialog = new AlertDialog.Builder(this);
        mAlertDialog = new AlertDialog.Builder(this, themeId);
        mAlertDialog.setView(mAlertView);
        mAlertDialog.setView(mAlertView);
        mAlertDialog.setOnCancelListener(dialog -> declineLogAccess());
        mAlertDialog.setOnDismissListener(dialog -> finish());


        // show Alert
        // show Alert
        mAlert = mAlertDialog.create();
        mAlert = mAlertDialog.create();
@@ -89,15 +100,6 @@ public class LogAccessDialogActivity extends Activity implements


        // set Alert Timeout
        // set Alert Timeout
        mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT);
        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);
            }
        }
    }
    }


    @Override
    @Override
@@ -109,21 +111,26 @@ public class LogAccessDialogActivity extends Activity implements
        mAlert = null;
        mAlert = null;
    }
    }


    private void getIntentInfo(Intent intent) throws Exception {
    private boolean readIntentInfo(Intent intent) {

        if (intent == null) {
        if (intent == null) {
            throw new NullPointerException("Intent is null");
            Slog.e(TAG, "Intent is null");
            return false;
        }
        }


        mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
        mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
        if (mPackageName == null || mPackageName.length() == 0) {
        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);
        if (!intent.hasExtra(Intent.EXTRA_UID)) {
        mGid = intent.getIntExtra("com.android.server.logcat.gid", 0);
            Slog.e(TAG, "Missing EXTRA_UID");
        mPid = intent.getIntExtra("com.android.server.logcat.pid", 0);
            return false;
        mFd = intent.getIntExtra("com.android.server.logcat.fd", 0);
        }

        mUid = intent.getIntExtra(Intent.EXTRA_UID, 0);

        return true;
    }
    }


    private Handler mHandler = new Handler() {
    private Handler mHandler = new Handler() {
@@ -133,11 +140,7 @@ public class LogAccessDialogActivity extends Activity implements
                    if (mAlert != null) {
                    if (mAlert != null) {
                        mAlert.dismiss();
                        mAlert.dismiss();
                        mAlert = null;
                        mAlert = null;
                        try {
                        declineLogAccess();
                            mLogcatManagerService.decline(mUid, mGid, mPid, mFd);
                        } catch (RemoteException e) {
                            Slog.e(TAG, "Fails to call remote functions", e);
                        }
                    }
                    }
                    break;
                    break;


@@ -148,25 +151,15 @@ public class LogAccessDialogActivity extends Activity implements
    };
    };


    private String getTitleString(Context context, String callingPackage, int uid)
    private String getTitleString(Context context, String callingPackage, int uid)
            throws Exception {
            throws NameNotFoundException {

        PackageManager pm = context.getPackageManager();
        PackageManager pm = context.getPackageManager();
        if (pm == null) {
            throw new NullPointerException("PackageManager is null");
        }


        CharSequence appLabel = pm.getApplicationInfoAsUser(callingPackage,
        CharSequence appLabel = pm.getApplicationInfoAsUser(callingPackage,
                PackageManager.MATCH_DIRECT_BOOT_AUTO,
                PackageManager.MATCH_DIRECT_BOOT_AUTO,
                UserHandle.getUserId(uid)).loadLabel(pm);
                UserHandle.getUserId(uid)).loadLabel(pm);
        if (appLabel == null || appLabel.length() == 0) {
            throw new NameNotFoundException("Application Label is null");
        }


        String titleString = context.getString(
        String titleString = context.getString(
                com.android.internal.R.string.log_access_confirmation_title, appLabel);
                com.android.internal.R.string.log_access_confirmation_title, appLabel);
        if (titleString == null || titleString.length() == 0) {
            throw new NullPointerException("Title is null");
        }


        return titleString;
        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
     * If we cannot retrieve the package name, it returns null and we decline the full device log
     * access
     * access
     */
     */
    private View createView() throws Exception {
    private View createView(@StyleRes int themeId) {

        Context themedContext = new ContextThemeWrapper(getApplicationContext(), themeId);
        final View view = getLayoutInflater().inflate(
        final View view = LayoutInflater.from(themedContext).inflate(
                R.layout.log_access_user_consent_dialog_permission, null /*root*/);
                R.layout.log_access_user_consent_dialog_permission, null /*root*/);


        if (view == null) {
        if (view == null) {
@@ -202,21 +195,17 @@ public class LogAccessDialogActivity extends Activity implements
    public void onClick(View view) {
    public void onClick(View view) {
        switch (view.getId()) {
        switch (view.getId()) {
            case R.id.log_access_dialog_allow_button:
            case R.id.log_access_dialog_allow_button:
                try {
                mLogcatManagerInternal.approveAccessForClient(mUid, mPackageName);
                    mLogcatManagerService.approve(mUid, mGid, mPid, mFd);
                } catch (RemoteException e) {
                    Slog.e(TAG, "Fails to call remote functions", e);
                }
                finish();
                finish();
                break;
                break;
            case R.id.log_access_dialog_deny_button:
            case R.id.log_access_dialog_deny_button:
                try {
                declineLogAccess();
                    mLogcatManagerService.decline(mUid, mGid, mPid, mFd);
                } catch (RemoteException e) {
                    Slog.e(TAG, "Fails to call remote functions", e);
                }
                finish();
                finish();
                break;
                break;
        }
        }
    }
    }

    private void declineLogAccess() {
        mLogcatManagerInternal.declineAccessForClient(mUid, mPackageName);
    }
}
}
+397 −131

File changed.

Preview size limit exceeded, changes collapsed.

+322 −0
Original line number Original line Diff line number Diff line
/*
 * 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();
    }
}