import android.annotation.UserIdInt;
import android.app.AppGlobals;
import android.app.AppOpsManager;
+import android.app.Notification;
+import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.util.ArraySet;
import android.util.Log;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.LegacyVpnInfo;
/**
* Update current state, dispaching event to listeners.
*/
- private void updateState(DetailedState detailedState, String reason) {
+ @VisibleForTesting
+ protected void updateState(DetailedState detailedState, String reason) {
if (LOGD) Log.d(TAG, "setting state=" + detailedState + ", reason=" + reason);
mNetworkInfo.setDetailedState(detailedState, reason, null);
if (mNetworkAgent != null) {
mNetworkAgent.sendNetworkInfo(mNetworkInfo);
}
+ updateAlwaysOnNotification(detailedState);
}
/**
}
mLockdown = (mAlwaysOn && lockdown);
- if (!isCurrentPreparedPackage(packageName)) {
+ if (isCurrentPreparedPackage(packageName)) {
+ updateAlwaysOnNotification(mNetworkInfo.getDetailedState());
+ } else {
+ // Prepare this app. The notification will update as a side-effect of updateState().
prepareInternal(packageName);
}
maybeRegisterPackageChangeReceiverLocked(packageName);
}
}
- private void agentDisconnect(NetworkInfo networkInfo, NetworkAgent networkAgent) {
- networkInfo.setIsAvailable(false);
- networkInfo.setDetailedState(DetailedState.DISCONNECTED, null, null);
+ private void agentDisconnect(NetworkAgent networkAgent) {
if (networkAgent != null) {
+ NetworkInfo networkInfo = new NetworkInfo(mNetworkInfo);
+ networkInfo.setIsAvailable(false);
+ networkInfo.setDetailedState(DetailedState.DISCONNECTED, null, null);
networkAgent.sendNetworkInfo(networkInfo);
}
}
- private void agentDisconnect(NetworkAgent networkAgent) {
- NetworkInfo networkInfo = new NetworkInfo(mNetworkInfo);
- agentDisconnect(networkInfo, networkAgent);
- }
-
private void agentDisconnect() {
if (mNetworkInfo.isConnected()) {
- agentDisconnect(mNetworkInfo, mNetworkAgent);
+ mNetworkInfo.setIsAvailable(false);
+ updateState(DetailedState.DISCONNECTED, "agentDisconnect");
mNetworkAgent = null;
}
}
}
}
+ private void updateAlwaysOnNotification(DetailedState networkState) {
+ final boolean visible = (mAlwaysOn && networkState != DetailedState.CONNECTED);
+ updateAlwaysOnNotificationInternal(visible);
+ }
+
+ @VisibleForTesting
+ protected void updateAlwaysOnNotificationInternal(boolean visible) {
+ final UserHandle user = UserHandle.of(mUserHandle);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final NotificationManager notificationManager = NotificationManager.from(mContext);
+ if (!visible) {
+ notificationManager.cancelAsUser(TAG, 0, user);
+ return;
+ }
+ final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS);
+ final PendingIntent configIntent = PendingIntent.getActivityAsUser(
+ mContext, /* request */ 0, intent,
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
+ null, user);
+ final Notification.Builder builder = new Notification.Builder(mContext)
+ .setDefaults(0)
+ .setSmallIcon(R.drawable.vpn_connected)
+ .setContentTitle(mContext.getString(R.string.vpn_lockdown_disconnected))
+ .setContentText(mContext.getString(R.string.vpn_lockdown_config))
+ .setContentIntent(configIntent)
+ .setCategory(Notification.CATEGORY_SYSTEM)
+ .setPriority(Notification.PRIORITY_LOW)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setOngoing(true)
+ .setColor(mContext.getColor(R.color.system_notification_accent_color));
+ notificationManager.notifyAsUser(TAG, 0, builder.build(), user);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
private native int jniCreate(int mtu);
private native String jniGetName(int tun);
private native int jniSetAddresses(String interfaze, String addresses);
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
+import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
+import android.net.NetworkInfo.DetailedState;
import android.net.UidRange;
import android.os.INetworkManagementService;
import android.os.Looper;
import java.util.Map;
import java.util.Set;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@Mock private PackageManager mPackageManager;
@Mock private INetworkManagementService mNetService;
@Mock private AppOpsManager mAppOps;
+ @Mock private NotificationManager mNotificationManager;
@Override
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
setMockedPackages(mPackages);
+ when(mContext.getPackageName()).thenReturn(Vpn.class.getPackage().getName());
when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE))).thenReturn(mAppOps);
+ when(mContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
+ .thenReturn(mNotificationManager);
doNothing().when(mNetService).registerObserver(any());
}
public void testRestrictedProfilesAreAddedToVpn() {
setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
- final Vpn vpn = new MockVpn(primaryUser.id);
+ final Vpn vpn = spyVpn(primaryUser.id);
final Set<UidRange> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
null, null);
public void testManagedProfilesAreNotAddedToVpn() {
setMockedUsers(primaryUser, managedProfileA);
- final Vpn vpn = new MockVpn(primaryUser.id);
+ final Vpn vpn = spyVpn(primaryUser.id);
final Set<UidRange> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
null, null);
public void testAddUserToVpnOnlyAddsOneUser() {
setMockedUsers(primaryUser, restrictedProfileA, managedProfileA);
- final Vpn vpn = new MockVpn(primaryUser.id);
+ final Vpn vpn = spyVpn(primaryUser.id);
final Set<UidRange> ranges = new ArraySet<>();
vpn.addUserToRanges(ranges, primaryUser.id, null, null);
@SmallTest
public void testUidWhiteAndBlacklist() throws Exception {
- final Vpn vpn = new MockVpn(primaryUser.id);
+ final Vpn vpn = spyVpn(primaryUser.id);
final UidRange user = UidRange.createForUser(primaryUser.id);
final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
@SmallTest
public void testLockdownChangingPackage() throws Exception {
- final MockVpn vpn = new MockVpn(primaryUser.id);
+ final Vpn vpn = spyVpn(primaryUser.id);
final UidRange user = UidRange.createForUser(primaryUser.id);
// Default state.
- vpn.assertUnblocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+ assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
// Set always-on without lockdown.
assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false));
- vpn.assertUnblocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+ assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
// Set always-on with lockdown.
assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true));
new UidRange(user.start, user.start + PKG_UIDS[1] - 1),
new UidRange(user.start + PKG_UIDS[1] + 1, user.stop)
}));
- vpn.assertBlocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
- vpn.assertUnblocked(user.start + PKG_UIDS[1]);
+ assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+ assertUnblocked(vpn, user.start + PKG_UIDS[1]);
// Switch to another app.
assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true));
new UidRange(user.start, user.start + PKG_UIDS[3] - 1),
new UidRange(user.start + PKG_UIDS[3] + 1, user.stop)
}));
- vpn.assertBlocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]);
- vpn.assertUnblocked(user.start + PKG_UIDS[3]);
+ assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]);
+ assertUnblocked(vpn, user.start + PKG_UIDS[3]);
}
@SmallTest
public void testLockdownAddingAProfile() throws Exception {
- final MockVpn vpn = new MockVpn(primaryUser.id);
+ final Vpn vpn = spyVpn(primaryUser.id);
setMockedUsers(primaryUser);
// Make a copy of the restricted profile, as we're going to mark it deleted halfway through.
}));
// Verify restricted user isn't affected at first.
- vpn.assertUnblocked(profile.start + PKG_UIDS[0]);
+ assertUnblocked(vpn, profile.start + PKG_UIDS[0]);
// Add the restricted user.
setMockedUsers(primaryUser, tempProfile);
}));
}
+ @SmallTest
+ public void testNotificationShownForAlwaysOnApp() {
+ final Vpn vpn = spyVpn(primaryUser.id);
+ final InOrder order = inOrder(vpn);
+ setMockedUsers(primaryUser);
+
+ // Don't show a notification for regular disconnected states.
+ vpn.updateState(DetailedState.DISCONNECTED, TAG);
+ order.verify(vpn).updateAlwaysOnNotificationInternal(false);
+
+ // Start showing a notification for disconnected once always-on.
+ vpn.setAlwaysOnPackage(PKGS[0], false);
+ order.verify(vpn).updateAlwaysOnNotificationInternal(true);
+
+ // Stop showing the notification once connected.
+ vpn.updateState(DetailedState.CONNECTED, TAG);
+ order.verify(vpn).updateAlwaysOnNotificationInternal(false);
+
+ // Show the notification if we disconnect again.
+ vpn.updateState(DetailedState.DISCONNECTED, TAG);
+ order.verify(vpn).updateAlwaysOnNotificationInternal(true);
+
+ // Notification should be cleared after unsetting always-on package.
+ vpn.setAlwaysOnPackage(null, false);
+ order.verify(vpn).updateAlwaysOnNotificationInternal(false);
+ }
+
/**
- * A subclass of {@link Vpn} with some of the fields pre-mocked.
+ * Mock some methods of vpn object.
*/
- private class MockVpn extends Vpn {
- public MockVpn(@UserIdInt int userId) {
- super(Looper.myLooper(), mContext, mNetService, userId);
- }
+ private Vpn spyVpn(@UserIdInt int userId) {
+ final Vpn vpn = spy(new Vpn(Looper.myLooper(), mContext, mNetService, userId));
- public void assertBlocked(int... uids) {
- for (int uid : uids) {
- assertTrue("Uid " + uid + " should be blocked", isBlockingUid(uid));
- }
+ // Block calls to the NotificationManager or PendingIntent#getActivity.
+ doNothing().when(vpn).updateAlwaysOnNotificationInternal(anyBoolean());
+ return vpn;
+ }
+
+ private static void assertBlocked(Vpn vpn, int... uids) {
+ for (int uid : uids) {
+ assertTrue("Uid " + uid + " should be blocked", vpn.isBlockingUid(uid));
}
+ }
- public void assertUnblocked(int... uids) {
- for (int uid : uids) {
- assertFalse("Uid " + uid + " should not be blocked", isBlockingUid(uid));
- }
+ private static void assertUnblocked(Vpn vpn, int... uids) {
+ for (int uid : uids) {
+ assertFalse("Uid " + uid + " should not be blocked", vpn.isBlockingUid(uid));
}
}