diff --git a/keystore/java/android/security/KeyChain.java b/keystore/java/android/security/KeyChain.java index e566b9d75846b..9981668b7cca3 100644 --- a/keystore/java/android/security/KeyChain.java +++ b/keystore/java/android/security/KeyChain.java @@ -588,7 +588,7 @@ public final class KeyChain { * @hide for reuse by CertInstaller and Settings. * @see KeyChain#bind */ - public final static class KeyChainConnection implements Closeable { + public static class KeyChainConnection implements Closeable { private final Context context; private final ServiceConnection serviceConnection; private final IKeyChainService service; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 003b6d0d3be9c..590a175e8efab 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -99,6 +99,7 @@ import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; +import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Color; @@ -208,7 +209,7 @@ import java.util.concurrent.atomic.AtomicBoolean; */ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { - private static final String LOG_TAG = "DevicePolicyManager"; + protected static final String LOG_TAG = "DevicePolicyManager"; private static final boolean VERBOSE_LOG = false; // DO NOT SUBMIT WITH TRUE @@ -252,7 +253,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private static final String ACTION_EXPIRED_PASSWORD_NOTIFICATION = "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION"; - private static final int MONITORING_CERT_NOTIFICATION_ID = R.plurals.ssl_ca_cert_warning; private static final int PROFILE_WIPED_NOTIFICATION_ID = 1001; private static final int NETWORK_LOGGING_NOTIFICATION_ID = 1002; @@ -409,6 +409,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } }; + /** Listens only if mHasFeature == true. */ private final BroadcastReceiver mRemoteBugreportFinishedReceiver = new BroadcastReceiver() { @Override @@ -420,6 +421,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } }; + /** Listens only if mHasFeature == true. */ private final BroadcastReceiver mRemoteBugreportConsentReceiver = new BroadcastReceiver() { @Override @@ -513,7 +515,21 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final Handler mHandler; - BroadcastReceiver mReceiver = new BroadcastReceiver() { + /** Listens on any device, even when mHasFeature == false. */ + final BroadcastReceiver mRootCaReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (StorageManager.inCryptKeeperBounce()) { + return; + } + final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId()); + new MonitoringCertNotificationTask(DevicePolicyManagerService.this, mInjector) + .execute(userHandle); + } + }; + + /** Listens only if mHasFeature == true. */ + final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); @@ -559,14 +575,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } }); } - if (Intent.ACTION_USER_UNLOCKED.equals(action) - || Intent.ACTION_USER_STARTED.equals(action) - || KeyChain.ACTION_TRUST_STORE_CHANGED.equals(action)) { - if (!StorageManager.inCryptKeeperBounce()) { - new MonitoringCertNotificationTask().execute( - intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_ALL)); - } - } + if (Intent.ACTION_USER_ADDED.equals(action)) { sendUserAddedOrRemovedCommand(DeviceAdminReceiver.ACTION_USER_ADDED, userHandle); synchronized (DevicePolicyManagerService.this) { @@ -1490,6 +1499,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mContext = context; } + Context createContextAsUser(UserHandle user) throws PackageManager.NameNotFoundException { + final String packageName = mContext.getPackageName(); + return mContext.createPackageContextAsUser(packageName, 0, user); + } + + Resources getResources() { + return mContext.getResources(); + } + Owners newOwners() { return new Owners(getUserManager(), getUserManagerInternal(), getPackageManagerInternal()); @@ -1725,6 +1743,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { boolean securityLogIsLoggingEnabled() { return SecurityLog.isLoggingEnabled(); } + + KeyChainConnection keyChainBindAsUser(UserHandle user) throws InterruptedException { + return KeyChain.bindAsUser(mContext, user); + } } /** @@ -1755,18 +1777,27 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { .hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN); mIsWatch = mInjector.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_WATCH); + + // Broadcast filter for changes to the trusted certificate store. These changes get a + // separate intent filter so we can listen to them even when device_admin is off. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_STARTED); + filter.addAction(Intent.ACTION_USER_UNLOCKED); + filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED); + filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); + mContext.registerReceiverAsUser(mRootCaReceiver, UserHandle.ALL, filter, null, mHandler); + if (!mHasFeature) { // Skip the rest of the initialization return; } - IntentFilter filter = new IntentFilter(); + + filter = new IntentFilter(); filter.addAction(Intent.ACTION_BOOT_COMPLETED); filter.addAction(ACTION_EXPIRED_PASSWORD_NOTIFICATION); filter.addAction(Intent.ACTION_USER_ADDED); filter.addAction(Intent.ACTION_USER_REMOVED); filter.addAction(Intent.ACTION_USER_STARTED); - filter.addAction(Intent.ACTION_USER_UNLOCKED); - filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); filter = new IntentFilter(); @@ -2946,125 +2977,34 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } - private class MonitoringCertNotificationTask extends AsyncTask { - @Override - protected Void doInBackground(Integer... params) { - int userHandle = params[0]; + /** + * Remove deleted CA certificates from the "approved" list for a particular user, counting + * the number still remaining to approve. + * + * @param userHandle user to check for. This must be a real user and not, for example, + * {@link UserHandle#ALL}. + * @param installedCertificates the full set of certificate authorities currently installed for + * {@param userHandle}. After calling this function, {@code mAcceptedCaCertificates} will + * correspond to some subset of this. + * + * @return number of certificates yet to be approved by {@param userHandle}. + */ + protected synchronized int retainAcceptedCertificates(final UserHandle userHandle, + final @NonNull Collection installedCertificates) { + enforceManageUsers(); - if (userHandle == UserHandle.USER_ALL) { - for (UserInfo userInfo : mUserManager.getUsers(true)) { - manageNotification(userInfo.getUserHandle()); - } - } else { - manageNotification(UserHandle.of(userHandle)); - } - return null; - } + if (!mHasFeature) { + return installedCertificates.size(); + } else { + final DevicePolicyData policy = getUserData(userHandle.getIdentifier()); - private void manageNotification(UserHandle userHandle) { - if (!mUserManager.isUserUnlocked(userHandle)) { - return; + // Remove deleted certificates. Flush xml if necessary. + if (policy.mAcceptedCaCertificates.retainAll(installedCertificates)) { + saveSettingsLocked(userHandle.getIdentifier()); } - // Call out to KeyChain to check for CAs which are waiting for approval. - final List pendingCertificates; - try { - pendingCertificates = getInstalledCaCertificates(userHandle); - } catch (RemoteException | RuntimeException e) { - Log.e(LOG_TAG, "Could not retrieve certificates from KeyChain service", e); - return; - } - - synchronized (DevicePolicyManagerService.this) { - final DevicePolicyData policy = getUserData(userHandle.getIdentifier()); - - // Remove deleted certificates. Flush xml if necessary. - if (policy.mAcceptedCaCertificates.retainAll(pendingCertificates)) { - saveSettingsLocked(userHandle.getIdentifier()); - } - // Trim to approved certificates. - pendingCertificates.removeAll(policy.mAcceptedCaCertificates); - } - - if (pendingCertificates.isEmpty()) { - mInjector.getNotificationManager().cancelAsUser( - null, MONITORING_CERT_NOTIFICATION_ID, userHandle); - return; - } - - // Build and show a warning notification - int smallIconId; - String contentText; - int parentUserId = userHandle.getIdentifier(); - if (getProfileOwner(userHandle.getIdentifier()) != null) { - contentText = mContext.getString(R.string.ssl_ca_cert_noti_managed, - getProfileOwnerName(userHandle.getIdentifier())); - smallIconId = R.drawable.stat_sys_certificate_info; - parentUserId = getProfileParentId(userHandle.getIdentifier()); - } else if (getDeviceOwnerUserId() == userHandle.getIdentifier()) { - contentText = mContext.getString(R.string.ssl_ca_cert_noti_managed, - getDeviceOwnerName()); - smallIconId = R.drawable.stat_sys_certificate_info; - } else { - contentText = mContext.getString(R.string.ssl_ca_cert_noti_by_unknown); - smallIconId = android.R.drawable.stat_sys_warning; - } - - final int numberOfCertificates = pendingCertificates.size(); - Intent dialogIntent = new Intent(Settings.ACTION_MONITORING_CERT_INFO); - dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - dialogIntent.setPackage("com.android.settings"); - dialogIntent.putExtra(Settings.EXTRA_NUMBER_OF_CERTIFICATES, numberOfCertificates); - dialogIntent.putExtra(Intent.EXTRA_USER_ID, userHandle.getIdentifier()); - PendingIntent notifyIntent = PendingIntent.getActivityAsUser(mContext, 0, - dialogIntent, PendingIntent.FLAG_UPDATE_CURRENT, null, - new UserHandle(parentUserId)); - - final Context userContext; - try { - final String packageName = mContext.getPackageName(); - userContext = mContext.createPackageContextAsUser(packageName, 0, userHandle); - } catch (PackageManager.NameNotFoundException e) { - Log.e(LOG_TAG, "Create context as " + userHandle + " failed", e); - return; - } - final Notification noti = new Notification.Builder(userContext) - .setSmallIcon(smallIconId) - .setContentTitle(mContext.getResources().getQuantityText( - R.plurals.ssl_ca_cert_warning, numberOfCertificates)) - .setContentText(contentText) - .setContentIntent(notifyIntent) - .setPriority(Notification.PRIORITY_HIGH) - .setShowWhen(false) - .setColor(mContext.getColor( - com.android.internal.R.color.system_notification_accent_color)) - .build(); - - mInjector.getNotificationManager().notifyAsUser( - null, MONITORING_CERT_NOTIFICATION_ID, noti, userHandle); - } - - private List getInstalledCaCertificates(UserHandle userHandle) - throws RemoteException, RuntimeException { - KeyChainConnection conn = null; - try { - conn = KeyChain.bindAsUser(mContext, userHandle); - List aliases = conn.getService().getUserCaAliases().getList(); - List result = new ArrayList<>(aliases.size()); - for (int i = 0; i < aliases.size(); i++) { - result.add(aliases.get(i).string); - } - return result; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return null; - } catch (AssertionError e) { - throw new RuntimeException(e); - } finally { - if (conn != null) { - conn.close(); - } - } + // Trim approved certificates from the count. + return installedCertificates.size() - policy.mAcceptedCaCertificates.size(); } } @@ -4629,7 +4569,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } saveSettingsLocked(userId); } - new MonitoringCertNotificationTask().execute(userId); + new MonitoringCertNotificationTask(this, mInjector).execute(userId); return true; } @@ -4653,7 +4593,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { saveSettingsLocked(userInfo.id); } - new MonitoringCertNotificationTask().execute(userInfo.id); + new MonitoringCertNotificationTask(this, mInjector).execute(userInfo.id); } } } @@ -7148,7 +7088,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return UserHandle.isSameApp(mInjector.binderGetCallingUid(), Process.SYSTEM_UID); } - private int getProfileParentId(int userHandle) { + protected int getProfileParentId(int userHandle) { final long ident = mInjector.binderClearCallingIdentity(); try { UserInfo parentUser = mUserManager.getProfileParent(userHandle); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/MonitoringCertNotificationTask.java b/services/devicepolicy/java/com/android/server/devicepolicy/MonitoringCertNotificationTask.java new file mode 100644 index 0000000000000..03c137a95b85a --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/MonitoringCertNotificationTask.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2017 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.devicepolicy; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Build; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.security.KeyChain.KeyChainConnection; +import android.util.Log; + +import com.android.internal.R; +import com.android.internal.util.ParcelableString; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class MonitoringCertNotificationTask extends AsyncTask { + protected static final String LOG_TAG = DevicePolicyManagerService.LOG_TAG; + protected static final int MONITORING_CERT_NOTIFICATION_ID = R.plurals.ssl_ca_cert_warning; + + private final DevicePolicyManagerService mService; + private final DevicePolicyManagerService.Injector mInjector; + + public MonitoringCertNotificationTask(final DevicePolicyManagerService service, + final DevicePolicyManagerService.Injector injector) { + super(); + mService = service; + mInjector = injector; + } + + @Override + protected Void doInBackground(Integer... params) { + int userHandle = params[0]; + + if (userHandle == UserHandle.USER_ALL) { + for (UserInfo userInfo : mInjector.getUserManager().getUsers(true)) { + repostOrClearNotification(userInfo.getUserHandle()); + } + } else { + repostOrClearNotification(UserHandle.of(userHandle)); + } + return null; + } + + private void repostOrClearNotification(UserHandle userHandle) { + if (!mInjector.getUserManager().isUserUnlocked(userHandle.getIdentifier())) { + return; + } + + // Call out to KeyChain to check for CAs which are waiting for approval. + final int pendingCertificateCount; + try { + pendingCertificateCount = mService.retainAcceptedCertificates( + userHandle, getInstalledCaCertificates(userHandle)); + } catch (RemoteException | RuntimeException e) { + Log.e(LOG_TAG, "Could not retrieve certificates from KeyChain service", e); + return; + } + + if (pendingCertificateCount != 0) { + showNotification(userHandle, pendingCertificateCount); + } else { + mInjector.getNotificationManager().cancelAsUser( + LOG_TAG, MONITORING_CERT_NOTIFICATION_ID, userHandle); + } + } + + private void showNotification(UserHandle userHandle, int pendingCertificateCount) { + // Create a context for the target user. + final Context userContext; + try { + userContext = mInjector.createContextAsUser(userHandle); + } catch (PackageManager.NameNotFoundException e) { + Log.e(LOG_TAG, "Create context as " + userHandle + " failed", e); + return; + } + + // Build and show a warning notification + int smallIconId; + String contentText; + int parentUserId = userHandle.getIdentifier(); + Resources resources = mInjector.getResources(); + if (mService.getProfileOwner(userHandle.getIdentifier()) != null) { + contentText = resources.getString(R.string.ssl_ca_cert_noti_managed, + mService.getProfileOwnerName(userHandle.getIdentifier())); + smallIconId = R.drawable.stat_sys_certificate_info; + parentUserId = mService.getProfileParentId(userHandle.getIdentifier()); + } else if (mService.getDeviceOwnerUserId() == userHandle.getIdentifier()) { + contentText = resources.getString(R.string.ssl_ca_cert_noti_managed, + mService.getDeviceOwnerName()); + smallIconId = R.drawable.stat_sys_certificate_info; + } else { + contentText = resources.getString(R.string.ssl_ca_cert_noti_by_unknown); + smallIconId = android.R.drawable.stat_sys_warning; + } + + Intent dialogIntent = new Intent(Settings.ACTION_MONITORING_CERT_INFO); + dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + // TODO this next line is taken from original notification code in + // {@link DevicePolicyManagerService} but not a very good way of doing it. Do it better. + dialogIntent.setPackage("com.android.settings"); + dialogIntent.putExtra(Settings.EXTRA_NUMBER_OF_CERTIFICATES, pendingCertificateCount); + dialogIntent.putExtra(Intent.EXTRA_USER_ID, userHandle.getIdentifier()); + PendingIntent notifyIntent = PendingIntent.getActivityAsUser(userContext, 0, + dialogIntent, PendingIntent.FLAG_UPDATE_CURRENT, null, + UserHandle.of(parentUserId)); + + final Notification noti = new Notification.Builder(userContext) + .setSmallIcon(smallIconId) + .setContentTitle(resources.getQuantityText(R.plurals.ssl_ca_cert_warning, + pendingCertificateCount)) + .setContentText(contentText) + .setContentIntent(notifyIntent) + .setPriority(Notification.PRIORITY_HIGH) + .setShowWhen(false) + .setColor(R.color.system_notification_accent_color) + .build(); + + mInjector.getNotificationManager().notifyAsUser( + LOG_TAG, MONITORING_CERT_NOTIFICATION_ID, noti, userHandle); + } + + private List getInstalledCaCertificates(UserHandle userHandle) + throws RemoteException, RuntimeException { + try (KeyChainConnection conn = mInjector.keyChainBindAsUser(userHandle)) { + List aliases = conn.getService().getUserCaAliases().getList(); + List result = new ArrayList<>(aliases.size()); + for (int i = 0; i < aliases.size(); i++) { + result.add(aliases.get(i).string); + } + return result; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } catch (AssertionError e) { + throw new RuntimeException(e); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java index 3b92a34b7b7a2..e6dd13fc15c2d 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java @@ -29,6 +29,7 @@ import android.os.PowerManagerInternal; import android.os.UserHandle; import android.os.UserManager; import android.os.UserManagerInternal; +import android.security.KeyChain; import android.telephony.TelephonyManager; import android.util.ArrayMap; import android.util.Pair; @@ -375,5 +376,10 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi boolean isBuildDebuggable() { return context.buildMock.isDebuggable; } + + @Override + KeyChain.KeyChainConnection keyChainBindAsUser(UserHandle user) { + return context.keyChainConnection; + } } } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 6fb65d59e31e7..a186b5956f4a0 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -21,6 +21,8 @@ import static android.os.UserManagerInternal.CAMERA_NOT_DISABLED; import android.Manifest.permission; import android.app.Activity; +import android.app.Notification; +import android.app.NotificationManager; import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; @@ -32,6 +34,7 @@ import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.graphics.Color; import android.net.IIpConnectivityMetrics; @@ -52,10 +55,13 @@ import android.util.ArraySet; import android.util.Pair; import com.android.internal.R; +import com.android.internal.util.ParcelableString; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.pm.UserRestrictionsUtils; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -76,13 +82,16 @@ import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -1193,6 +1202,53 @@ public class DevicePolicyManagerTest extends DpmTestBase { return uid; } + public void testCertificateDisclosure() throws Exception { + final int userId = DpmMockContext.CALLER_USER_HANDLE; + final UserHandle user = UserHandle.of(userId); + + mContext.applicationInfo = new ApplicationInfo(); + mContext.callerPermissions.add(permission.MANAGE_USERS); + mContext.packageName = "com.android.frameworks.servicestests"; + mContext.userContexts.put(user, mContext); + when(mContext.resources.getColor(anyInt(), anyObject())).thenReturn(Color.WHITE); + + ParceledListSlice oneCert = asSlice(new String[] {"1"}); + ParceledListSlice fourCerts = asSlice(new String[] {"1", "2", "3", "4"}); + + final String TEST_STRING = "Test for exactly 2 certs out of 4"; + doReturn(TEST_STRING).when(mContext.resources).getQuantityText(anyInt(), eq(2)); + + // Given that we have exactly one certificate installed, + when(mContext.keyChainConnection.getService().getUserCaAliases()).thenReturn(oneCert); + // when that certificate is approved, + dpms.approveCaCert(oneCert.getList().get(0).string, userId, true); + // a notification should not be shown. + verify(mContext.notificationManager, timeout(1000)) + .cancelAsUser(anyString(), anyInt(), eq(user)); + + // Given that we have four certificates installed, + when(mContext.keyChainConnection.getService().getUserCaAliases()).thenReturn(fourCerts); + // when two of them are approved (one of them approved twice hence no action), + dpms.approveCaCert(fourCerts.getList().get(0).string, userId, true); + dpms.approveCaCert(fourCerts.getList().get(1).string, userId, true); + // a notification should be shown saying that there are two certificates left to approve. + verify(mContext.notificationManager, timeout(1000)) + .notifyAsUser(anyString(), anyInt(), argThat( + new BaseMatcher() { + @Override + public boolean matches(Object item) { + final Notification noti = (Notification) item; + return TEST_STRING.equals( + noti.extras.getString(Notification.EXTRA_TITLE)); + } + @Override + public void describeTo(Description description) { + description.appendText( + "Notification{title=\"" + TEST_STRING + "\"}"); + } + }), eq(user)); + } + /** * Simple test for delegate set/get and general delegation. Tests verifying that delegated * privileges can acually be exercised by a delegate are not covered here. @@ -3734,4 +3790,20 @@ public class DevicePolicyManagerTest extends DpmTestBase { assertTrue(dpm.setProfileOwner(admin, null, userId)); mContext.callerPermissions.removeAll(OWNER_SETUP_PERMISSIONS); } + + /** + * Convert String[] to ParceledListSlice<ParcelableString>. + *

+ * TODO: This shouldn't be necessary. If ParcelableString does need to exist, it also needs + * a real constructor. + */ + private static ParceledListSlice asSlice(String[] s) { + List list = new ArrayList<>(s.length); + for (int i = 0; i < s.length; i++) { + ParcelableString item = new ParcelableString(); + item.string = s[i]; + list.add(i, item); + } + return new ParceledListSlice(list); + } } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java index 22cd135923bbf..46aaf832ec99a 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java @@ -43,9 +43,11 @@ import android.os.PowerManagerInternal; import android.os.UserHandle; import android.os.UserManager; import android.os.UserManagerInternal; +import android.security.KeyChain; import android.telephony.TelephonyManager; import android.test.mock.MockContentResolver; import android.test.mock.MockContext; +import android.util.ArrayMap; import android.view.IWindowManager; import com.android.internal.widget.LockPatternUtils; @@ -58,10 +60,12 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -290,6 +294,7 @@ public class DpmMockContext extends MockContext { public final TelephonyManager telephonyManager; public final AccountManager accountManager; public final AlarmManager alarmManager; + public final KeyChain.KeyChainConnection keyChainConnection; /** Note this is a partial mock, not a real mock. */ public final PackageManager packageManager; @@ -300,6 +305,9 @@ public class DpmMockContext extends MockContext { public final BuildMock buildMock = new BuildMock(); + /** Optional mapping of other user contexts for {@link #createPackageContextAsUser} to return */ + public final Map userContexts = new ArrayMap<>(); + public String packageName = null; public ApplicationInfo applicationInfo = null; @@ -335,6 +343,7 @@ public class DpmMockContext extends MockContext { telephonyManager = mock(TelephonyManager.class); accountManager = mock(AccountManager.class); alarmManager = mock(AlarmManager.class); + keyChainConnection = mock(KeyChain.KeyChainConnection.class, RETURNS_DEEP_STUBS); // Package manager is huge, so we use a partial mock instead. packageManager = spy(context.getPackageManager()); @@ -689,6 +698,19 @@ public class DpmMockContext extends MockContext { spiedContext.unregisterReceiver(receiver); } + @Override + public Context createPackageContextAsUser(String packageName, int flags, UserHandle user) + throws PackageManager.NameNotFoundException { + if (!userContexts.containsKey(user)) { + return super.createPackageContextAsUser(packageName, flags, user); + } + if (!getPackageName().equals(packageName)) { + throw new UnsupportedOperationException( + "Creating a context as another package is not implemented"); + } + return userContexts.get(user); + } + @Override public ContentResolver getContentResolver() { return contentResolver;