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

Commit 9c953855 authored by Lucas Silva's avatar Lucas Silva
Browse files

Implement SLS allowlist using App Compat framework.

TODO (in follow-up):
- modify app compat to check signatures
- add showWhenLocked check (breaks some code which should be fixed first)

Test: locally on device
Test: atest FrameworksMockingServicesTests:CommunalManagerServiceTest
Bug: 203673428
Bug: 203692863
Bug: 203684016
Change-Id: I00e1ee0b04bc5a7fec4a32af883d8d7cfed11c0c
parent e111dd61
Loading
Loading
Loading
Loading
+72 −17
Original line number Diff line number Diff line
@@ -23,14 +23,20 @@ import static com.android.server.wm.ActivityInterceptorCallback.COMMUNAL_MODE_OR
import android.Manifest;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.TestApi;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.app.communal.ICommunalManager;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.Disabled;
import android.compat.annotation.Overridable;
import android.content.Context;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
@@ -38,6 +44,8 @@ import android.os.Looper;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.LaunchAfterAuthenticationActivity;
@@ -55,6 +63,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
 * System service for handling Communal Mode state.
 */
public final class CommunalManagerService extends SystemService {
    private static final String TAG = CommunalManagerService.class.getSimpleName();
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final String DELIMITER = ",";
    private final Context mContext;
    private final ActivityTaskManagerInternal mAtmInternal;
@@ -63,13 +73,37 @@ public final class CommunalManagerService extends SystemService {
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final Set<String> mEnabledApps = new HashSet<>();
    private final SettingsObserver mSettingsObserver;
    private final BinderService mBinderService;

    /**
     * This change id is used to annotate packages which are allowed to run in communal mode.
     *
     * @hide
     */
    @ChangeId
    @Overridable
    @Disabled
    @TestApi
    public static final long ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT = 200324021L;

    /**
     * This change id is used to annotate packages which can run in communal mode by default,
     * without requiring user opt-in.
     *
     * @hide
     */
    @ChangeId
    @Overridable
    @Disabled
    @TestApi
    public static final long ALLOW_COMMUNAL_MODE_BY_DEFAULT = 203673428L;

    private final ActivityInterceptorCallback mActivityInterceptorCallback =
            new ActivityInterceptorCallback() {
                @Nullable
                @Override
                public Intent intercept(ActivityInterceptorInfo info) {
                    if (isActivityAllowed(info.aInfo)) {
                    if (!shouldIntercept(info.aInfo)) {
                        return null;
                    }

@@ -99,15 +133,25 @@ public final class CommunalManagerService extends SystemService {
        mSettingsObserver = new SettingsObserver();
        mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
        mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
        mBinderService = new BinderService();
    }

    @VisibleForTesting
    BinderService getBinderServiceInstance() {
        return mBinderService;
    }

    @VisibleForTesting
    void publishBinderServices() {
        publishBinderService(Context.COMMUNAL_MANAGER_SERVICE, mBinderService);
    }

    @Override
    public void onStart() {
        publishBinderService(Context.COMMUNAL_MANAGER_SERVICE, new BinderService());
        publishBinderServices();
        mAtmInternal.registerActivityStartInterceptor(COMMUNAL_MODE_ORDERED_ID,
                mActivityInterceptorCallback);


        updateSelectedApps();
        mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
                Settings.Secure.COMMUNAL_MODE_PACKAGES), false, mSettingsObserver,
@@ -128,20 +172,31 @@ public final class CommunalManagerService extends SystemService {
        }
    }

    private boolean isActivityAllowed(ActivityInfo activityInfo) {
    private boolean isAppAllowed(ApplicationInfo appInfo) {
        if (isChangeEnabled(ALLOW_COMMUNAL_MODE_BY_DEFAULT, appInfo)) {
            return true;
        // TODO(b/191994709): Uncomment the lines below once Dreams and Assistant have been fixed.
//        if (!mCommunalViewIsShowing.get() || !mKeyguardManager.isKeyguardLocked()) return true;
//
//        // If the activity doesn't have showWhenLocked enabled, disallow the activity.
//        final boolean showWhenLocked =
//                (activityInfo.flags & ActivityInfo.FLAG_SHOW_WHEN_LOCKED) != 0;
//        if (!showWhenLocked) {
//            return false;
//        }
//
//        // Check the cached user preferences to see if the user has allowed this app.
//        return mEnabledApps.contains(activityInfo.applicationInfo.packageName);
        }

        if (appInfo.isSystemApp() || appInfo.isUpdatedSystemApp()) {
            if (DEBUG) Slog.d(TAG, "Allowlisted as system app: " + appInfo.packageName);
            return isAppEnabledByUser(appInfo);
        }

        return isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, appInfo)
                && isAppEnabledByUser(appInfo);
    }

    private boolean isAppEnabledByUser(ApplicationInfo appInfo) {
        return mEnabledApps.contains(appInfo.packageName);
    }

    private boolean isChangeEnabled(long changeId, ApplicationInfo appInfo) {
        return CompatChanges.isChangeEnabled(changeId, appInfo.packageName, UserHandle.SYSTEM);
    }

    private boolean shouldIntercept(ActivityInfo activityInfo) {
        if (!mCommunalViewIsShowing.get() || !mKeyguardManager.isKeyguardLocked()) return false;
        return !isAppAllowed(activityInfo.applicationInfo);
    }

    private final class SettingsObserver extends ContentObserver {
+69 −36
Original line number Diff line number Diff line
@@ -18,34 +18,38 @@ package com.android.server.communal;

import static android.content.pm.ActivityInfo.FLAG_SHOW_WHEN_LOCKED;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.communal.CommunalManagerService.ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT;
import static com.android.server.wm.ActivityInterceptorCallback.COMMUNAL_MODE_ORDERED_ID;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;

import android.Manifest;
import android.app.KeyguardManager;
import android.app.communal.ICommunalManager;
import android.content.ContentResolver;
import android.content.Context;
import android.app.compat.CompatChanges;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
import android.test.mock.MockContentResolver;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;

import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.LocalServices;
import com.android.server.wm.ActivityInterceptorCallback;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -83,32 +87,35 @@ public class CommunalManagerServiceTest {
    private ActivityTaskManagerInternal mAtmInternal;
    @Mock
    private KeyguardManager mKeyguardManager;
    @Mock
    private Context mMockContext;
    @Mock
    private ContentResolver mContentResolver;

    private ActivityInterceptorCallback mActivityInterceptorCallback;
    private ActivityInfo mAInfo;
    private ICommunalManager mBinder;
    private ContextWrapper mContextSpy;

    @Before
    public final void setUp() {
        mMockingSession = mockitoSession()
                .initMocks(this)
                .mockStatic(LocalServices.class)
                .mockStatic(ServiceManager.class)
                .mockStatic(Settings.Secure.class)
                .mockStatic(CompatChanges.class)
                .mockStatic(KeyguardManager.class)
                .strictness(Strictness.WARN)
                .startMocking();

        when(mMockContext.getContentResolver()).thenReturn(mContentResolver);
        when(mMockContext.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager);
        doReturn(mAtmInternal).when(() -> LocalServices.getService(
                ActivityTaskManagerInternal.class));
        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
        MockContentResolver cr = new MockContentResolver(mContextSpy);
        cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
        when(mContextSpy.getContentResolver()).thenReturn(cr);

        when(mContextSpy.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager);
        addLocalServiceMock(ActivityTaskManagerInternal.class, mAtmInternal);

        mService = new CommunalManagerService(mMockContext);
        doNothing().when(mContextSpy).enforceCallingPermission(
                eq(Manifest.permission.WRITE_COMMUNAL_STATE), anyString());

        mService = new CommunalManagerService(mContextSpy);
        spyOn(mService);
        doNothing().when(mService).publishBinderServices();
        mService.onStart();

        ArgumentCaptor<ActivityInterceptorCallback> activityInterceptorCaptor =
@@ -117,11 +124,7 @@ public class CommunalManagerServiceTest {
                activityInterceptorCaptor.capture());
        mActivityInterceptorCallback = activityInterceptorCaptor.getValue();

        ArgumentCaptor<IBinder> binderCaptor = ArgumentCaptor.forClass(IBinder.class);
        verify(() -> ServiceManager.addService(eq(Context.COMMUNAL_MANAGER_SERVICE),
                binderCaptor.capture(),
                anyBoolean(), anyInt()));
        mBinder = ICommunalManager.Stub.asInterface(binderCaptor.getValue());
        mBinder = mService.getBinderServiceInstance();

        mAInfo = new ActivityInfo();
        mAInfo.applicationInfo = new ApplicationInfo();
@@ -130,11 +133,20 @@ public class CommunalManagerServiceTest {

    @After
    public void tearDown() {
        FakeSettingsProvider.clearSettingsProvider();
        if (mMockingSession != null) {
            mMockingSession.finishMocking();
        }
    }

    /**
     * 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);
    }

    private ActivityInterceptorCallback.ActivityInterceptorInfo buildActivityInfo(Intent intent) {
        return new ActivityInterceptorCallback.ActivityInterceptorInfo(
                TEST_REAL_CALLING_UID,
@@ -152,9 +164,8 @@ public class CommunalManagerServiceTest {
    }

    private void allowPackages(String packages) {
        doReturn(packages).when(
                () -> Settings.Secure.getStringForUser(mContentResolver,
                        Settings.Secure.COMMUNAL_MODE_PACKAGES, UserHandle.USER_SYSTEM));
        Settings.Secure.putStringForUser(mContextSpy.getContentResolver(),
                Settings.Secure.COMMUNAL_MODE_PACKAGES, packages, UserHandle.USER_SYSTEM);
        mService.updateSelectedApps();
    }

@@ -185,51 +196,73 @@ public class CommunalManagerServiceTest {
    }

    @Test
    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOff()
    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOff_allowlistEnabled()
            throws RemoteException {
        final Intent intent = new Intent(Intent.ACTION_MAIN);
        mBinder.setCommunalViewShowing(true);
        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
                UserHandle.SYSTEM)).thenReturn(true);
        mAInfo.flags = 0;
        // TODO(b/191994709): Fix this assertion once we properly intercept activities.
        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNotNull();
    }

    @Test
    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOn()
    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOn_allowlistEnabled()
            throws RemoteException {
        final Intent intent = new Intent(Intent.ACTION_MAIN);
        mBinder.setCommunalViewShowing(true);
        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
                UserHandle.SYSTEM)).thenReturn(true);
        mAInfo.flags = FLAG_SHOW_WHEN_LOCKED;

        allowPackages("package1,package2");
        // TODO(b/191994709): Fix this assertion once we properly intercept activities.
        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNotNull();
    }

    @Test
    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOff()
    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOff_allowlistEnabled()
            throws RemoteException {
        final Intent intent = new Intent(Intent.ACTION_MAIN);
        mBinder.setCommunalViewShowing(true);
        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
                UserHandle.SYSTEM)).thenReturn(true);
        mAInfo.flags = 0;

        allowPackages(TEST_PACKAGE_NAME);
        // TODO(b/191994709): Fix this assertion once we properly intercept activities.
        // TODO(b/191994709): Fix this assertion once we start checking showWhenLocked
        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
    }

    @Test
    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOn()
    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOn_allowlistEnabled()
            throws RemoteException {
        final Intent intent = new Intent(Intent.ACTION_MAIN);
        mBinder.setCommunalViewShowing(true);
        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
                UserHandle.SYSTEM)).thenReturn(true);

        mAInfo.flags = FLAG_SHOW_WHEN_LOCKED;

        allowPackages(TEST_PACKAGE_NAME);
        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
    }

    @Test
    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOn_allowlistDisabled()
            throws RemoteException {
        final Intent intent = new Intent(Intent.ACTION_MAIN);
        mBinder.setCommunalViewShowing(true);
        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
                UserHandle.SYSTEM)).thenReturn(false);

        mAInfo.flags = FLAG_SHOW_WHEN_LOCKED;

        allowPackages(TEST_PACKAGE_NAME);
        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNotNull();
    }
}