OSDN Git Service

Show notification for always-on app VPN
authorTony Mak <tonymak@google.com>
Thu, 30 Jun 2016 10:19:20 +0000 (11:19 +0100)
committerRobin Lee <rgl@google.com>
Thu, 6 Oct 2016 19:58:04 +0000 (19:58 +0000)
This is the same notification as the one shown during legacy lockdown
mode, sans the 'reset' button.

The notification is only shown during times when VPN has not yet
established or has failed, for example during boot or after a crash.

Bug: 29123115
Change-Id: If0e2eb4f8fb220a21d6d462363a05869e27c7b6e

services/core/java/com/android/server/connectivity/Vpn.java
services/tests/servicestests/src/com/android/server/connectivity/VpnTest.java

index ede3bda..afc6247 100644 (file)
@@ -27,6 +27,8 @@ import android.annotation.Nullable;
 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;
@@ -76,6 +78,7 @@ import android.text.TextUtils;
 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;
@@ -241,12 +244,14 @@ public class Vpn {
     /**
      * 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);
     }
 
     /**
@@ -280,7 +285,10 @@ public class Vpn {
         }
 
         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);
@@ -682,22 +690,19 @@ public class Vpn {
         }
     }
 
-    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;
         }
     }
@@ -1250,6 +1255,43 @@ public class Vpn {
         }
     }
 
+    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);
index 5d8b843..b51b277 100644 (file)
@@ -25,9 +25,11 @@ import static org.mockito.Mockito.*;
 
 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;
@@ -43,6 +45,8 @@ import java.util.Arrays;
 import java.util.Map;
 import java.util.Set;
 
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -88,14 +92,18 @@ public class VpnTest extends AndroidTestCase {
     @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());
     }
 
@@ -103,7 +111,7 @@ public class VpnTest extends AndroidTestCase {
     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);
 
@@ -117,7 +125,7 @@ public class VpnTest extends AndroidTestCase {
     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);
 
@@ -130,7 +138,7 @@ public class VpnTest extends AndroidTestCase {
     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);
 
@@ -141,7 +149,7 @@ public class VpnTest extends AndroidTestCase {
 
     @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]};
 
@@ -166,15 +174,15 @@ public class VpnTest extends AndroidTestCase {
 
     @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));
@@ -182,8 +190,8 @@ public class VpnTest extends AndroidTestCase {
             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));
@@ -195,13 +203,13 @@ public class VpnTest extends AndroidTestCase {
             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.
@@ -220,7 +228,7 @@ public class VpnTest extends AndroidTestCase {
         }));
 
         // 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);
@@ -239,24 +247,53 @@ public class VpnTest extends AndroidTestCase {
         }));
     }
 
+    @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));
         }
     }