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

Commit f9b28756 authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Add code for interfacing with PM

Allowing NMS to query app notification permission state

Test: PermissionHelperTest
Bug: 194833441
Change-Id: I8d9f7c7e72855abfa286c60ccf1ed4f5fe4f4128
parent 7c7bb9ab
Loading
Loading
Loading
Loading
+160 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.notification;

import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.permission.PermissionManager.PERMISSION_GRANTED;

import android.Manifest;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.ParceledListSlice;
import android.os.RemoteException;
import android.permission.IPermissionManager;
import android.util.Slog;

import com.android.server.pm.permission.PermissionManagerServiceInternal;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * NotificationManagerService helper for querying/setting the app-level notification permission
 */
public final class PermissionHelper {
    private static final String TAG = "PermissionHelper";

    private static String NOTIFICATION_PERMISSION = Manifest.permission.POST_NOTIFICATIONS;

    private final PermissionManagerServiceInternal mPmi;
    private final IPackageManager mPackageManager;
    private final IPermissionManager mPermManager;
    // TODO (b/194833441): Remove when the migration is enabled
    private final boolean mMigrationEnabled;

    public PermissionHelper(PermissionManagerServiceInternal pmi, IPackageManager packageManager,
            IPermissionManager permManager, boolean migrationEnabled) {
        mPmi = pmi;
        mPackageManager = packageManager;
        mPermManager = permManager;
        mMigrationEnabled = migrationEnabled;
    }

    /**
     * Returns whether the given uid holds the notification permission. Must not be called
     * with a lock held.
     */
    public boolean hasPermission(int uid) {
        assertFlag();
        return mPmi.checkUidPermission(uid, NOTIFICATION_PERMISSION) == PERMISSION_GRANTED;
    }

    /**
     * Returns all of the apps that have requested the notification permission in a given user.
     * Must not be called with a lock held. Format: uid, packageName
     */
    public Map<Integer, String> getAppsRequestingPermission(int userId) {
        assertFlag();
        Map<Integer, String> requested = new HashMap<>();
        List<PackageInfo> pkgs = getInstalledPackages(userId);
        for (PackageInfo pi : pkgs) {
            // when data was stored in PreferencesHelper, we only had data for apps that
            // had ever registered an intent to send a notification. To match that behavior,
            // filter the app list to apps that have requested the notification permission.
            if (pi.requestedPermissions == null) {
                continue;
            }
            for (String perm : pi.requestedPermissions) {
                if (NOTIFICATION_PERMISSION.equals(perm)) {
                    requested.put(pi.applicationInfo.uid, pi.packageName);
                    break;
                }
            }
        }
        return requested;
    }

    private List<PackageInfo> getInstalledPackages(int userId) {
        ParceledListSlice<PackageInfo> parceledList = null;
        try {
            parceledList = mPackageManager.getInstalledPackages(GET_PERMISSIONS, userId);
        } catch (RemoteException e) {
            Slog.d(TAG, "Could not reach system server", e);
        }
        if (parceledList == null) {
            return Collections.emptyList();
        }
        return parceledList.getList();
    }

    /**
     * Returns a list of apps that hold the notification permission. Must not be called
     * with a lock held. Format: uid, packageName.
     */
    public Map<Integer, String> getAppsGrantedPermission(int userId) {
        assertFlag();
        Map<Integer, String> granted = new HashMap<>();
        ParceledListSlice<PackageInfo> parceledList = null;
        try {
            parceledList = mPackageManager.getPackagesHoldingPermissions(
                    new String[] {NOTIFICATION_PERMISSION}, 0, userId);
        } catch (RemoteException e) {
            Slog.e(TAG, "Could not reach system server", e);
        }
        if (parceledList == null) {
            return granted;
        }
        for (PackageInfo pi : parceledList.getList()) {
            granted.put(pi.applicationInfo.uid, pi.packageName);
        }
        return granted;
    }

    /**
     * Grants or revokes the notification permission for a given package/user. UserSet should
     * only be true if this method is being called to migrate existing user choice, because it
     * can prevent the user from seeing the in app permission dialog. Must not be called
     * with a lock held.
     */
    public void setNotificationPermission(String packageName, int userId, boolean grant,
            boolean userSet) {
        assertFlag();
        try {
            if (grant) {
                mPermManager.grantRuntimePermission(packageName, NOTIFICATION_PERMISSION, userId);
            } else {
                mPermManager.revokeRuntimePermission(packageName, NOTIFICATION_PERMISSION, userId,
                        TAG);
            }
            if (userSet) {
                mPermManager.updatePermissionFlags(packageName, NOTIFICATION_PERMISSION,
                        FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, userId);
            }
        } catch (RemoteException e) {
            Slog.e(TAG, "Could not reach system server", e);
        }
    }

    private void assertFlag() {
        if (!mMigrationEnabled) {
            throw new IllegalStateException("Method called without checking flag value");
        }
    }
}
+245 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.notification;

import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.permission.PermissionManager.PERMISSION_GRANTED;
import static android.permission.PermissionManager.PERMISSION_SOFT_DENIED;

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

import static junit.framework.Assert.fail;

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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.Manifest;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.ParceledListSlice;
import android.permission.IPermissionManager;
import android.test.suitebuilder.annotation.SmallTest;

import androidx.test.runner.AndroidJUnit4;

import com.android.server.UiServiceTestCase;
import com.android.server.pm.permission.PermissionManagerServiceInternal;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class PermissionHelperTest extends UiServiceTestCase {

    @Mock
    private PermissionManagerServiceInternal mPmi;
    @Mock
    private IPackageManager mPackageManager;
    @Mock
    private IPermissionManager mPermManager;

    private PermissionHelper mPermissionHelper;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager, true);
    }

    // TODO (b/194833441): Remove when the migration is enabled
    @Test
    public void testMethodsThrowIfMigrationDisabled() throws IllegalAccessException,
            InvocationTargetException {
        PermissionHelper permHelper =
                new PermissionHelper(mPmi, mPackageManager, mPermManager, false);

        Method[] allMethods = PermissionHelper.class.getDeclaredMethods();
        for (Method method : allMethods) {
            if (Modifier.isPublic(method.getModifiers())) {
                Parameter[] params = method.getParameters();
                List<Object> args = Lists.newArrayListWithCapacity(params.length);
                for (int i = 0; i < params.length; i++) {
                    Type type = params[i].getParameterizedType();
                    if (type.getTypeName().equals("java.lang.String")) {
                        args.add("");
                    } else if (type.getTypeName().equals("boolean")){
                        args.add(false);
                    } else if (type.getTypeName().equals("int")) {
                        args.add(1);
                    }
                }
                try {
                    method.invoke(permHelper, args.toArray());
                    fail("Method should have thrown because migration flag is disabled");
                } catch (InvocationTargetException e) {
                    if (!(e.getTargetException() instanceof IllegalStateException)) {
                        throw e;
                    }
                }
            }
        }
    }

    @Test
    public void testHasPermission() throws Exception {
        when(mPmi.checkUidPermission(anyInt(), eq(Manifest.permission.POST_NOTIFICATIONS)))
                .thenReturn(PERMISSION_GRANTED);

        assertThat(mPermissionHelper.hasPermission(1)).isTrue();

        when(mPmi.checkUidPermission(anyInt(), eq(Manifest.permission.POST_NOTIFICATIONS)))
                .thenReturn(PERMISSION_SOFT_DENIED);

        assertThat(mPermissionHelper.hasPermission(1)).isFalse();
    }

    @Test
    public void testGetAppsRequestingPermission() throws Exception {
        // App that does not request permission
        PackageInfo notThis = new PackageInfo();
        notThis.packageName = "wrong.permission";
        notThis.requestedPermissions = new String[] {"something else"};
        // App that does not request any permissions (null check
        PackageInfo none = new PackageInfo();
        none.packageName = "no.permissions";
        // 2 apps that request the permission
        PackageInfo first = new PackageInfo();
        first.packageName = "first";
        first.requestedPermissions =
                new String[] {"something else", Manifest.permission.POST_NOTIFICATIONS};
        ApplicationInfo aiFirst = new ApplicationInfo();
        aiFirst.uid = 1;
        first.applicationInfo = aiFirst;
        PackageInfo second = new PackageInfo();
        second.packageName = "second";
        second.requestedPermissions = new String[] {Manifest.permission.POST_NOTIFICATIONS};
        ApplicationInfo aiSecond = new ApplicationInfo();
        aiSecond.uid = 2;
        second.applicationInfo = aiSecond;

        Map<Integer, String> expected = ImmutableMap.of(1, "first", 2, "second");

        ParceledListSlice<PackageInfo> infos = new ParceledListSlice<>(
                ImmutableList.of(notThis, none, first, second));
        when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS), anyInt())).thenReturn(infos);

        Map<Integer, String> actual = mPermissionHelper.getAppsRequestingPermission(0);

        assertThat(actual).containsExactlyEntriesIn(expected);
    }

    @Test
    public void testGetAppsGrantedPermission_noApps() throws Exception {
        int userId = 1;
        ParceledListSlice<PackageInfo> infos = ParceledListSlice.emptyList();
        when(mPackageManager.getPackagesHoldingPermissions(
                eq(new String[] {Manifest.permission.POST_NOTIFICATIONS}), anyInt(), eq(userId)))
                .thenReturn(infos);
        assertThat(mPermissionHelper.getAppsGrantedPermission(userId)).isNotNull();
    }

    @Test
    public void testGetAppsGrantedPermission() throws Exception {
        int userId = 1;
        PackageInfo first = new PackageInfo();
        first.packageName = "first";
        first.requestedPermissions =
                new String[] {"something else", Manifest.permission.POST_NOTIFICATIONS};
        ApplicationInfo aiFirst = new ApplicationInfo();
        aiFirst.uid = 1;
        first.applicationInfo = aiFirst;
        PackageInfo second = new PackageInfo();
        second.packageName = "second";
        second.requestedPermissions = new String[] {Manifest.permission.POST_NOTIFICATIONS};
        ApplicationInfo aiSecond = new ApplicationInfo();
        aiSecond.uid = 2;
        second.applicationInfo = aiSecond;

        ParceledListSlice<PackageInfo> infos = new ParceledListSlice<>(
                ImmutableList.of(first, second));
        when(mPackageManager.getPackagesHoldingPermissions(
                eq(new String[] {Manifest.permission.POST_NOTIFICATIONS}), anyInt(), eq(userId)))
                .thenReturn(infos);

        Map<Integer, String> expected = ImmutableMap.of(1, "first", 2, "second");

        assertThat(mPermissionHelper.getAppsGrantedPermission(userId))
                .containsExactlyEntriesIn(expected);
    }

    @Test
    public void testSetNotificationPermission_grantUserSet() throws Exception {
        mPermissionHelper.setNotificationPermission("pkg", 10, true, true);

        verify(mPermManager).grantRuntimePermission(
                "pkg", Manifest.permission.POST_NOTIFICATIONS, 10);
        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
                FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10);
    }

    @Test
    public void testSetNotificationPermission_revokeUserSet() throws Exception {
        mPermissionHelper.setNotificationPermission("pkg", 10, false, true);

        verify(mPermManager).revokeRuntimePermission(
                eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString());
        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
                FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10);
    }

    @Test
    public void testSetNotificationPermission_grantNotUserSet() throws Exception {
        mPermissionHelper.setNotificationPermission("pkg", 10, true, false);

        verify(mPermManager).grantRuntimePermission(
                "pkg", Manifest.permission.POST_NOTIFICATIONS, 10);
        verify(mPermManager, never()).updatePermissionFlags(
                anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt());
    }

    @Test
    public void testSetNotificationPermission_revokeNotUserSet() throws Exception {
        mPermissionHelper.setNotificationPermission("pkg", 10, false, false);

        verify(mPermManager).revokeRuntimePermission(
                eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString());
        verify(mPermManager, never()).updatePermissionFlags(
                anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt());
    }
}