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

Commit c12469bd authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Show notification for always-on app VPN"

parents 8e33c15f 1a405fe3
Loading
Loading
Loading
Loading
+53 −11
Original line number Diff line number Diff line
@@ -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) {
    private void agentDisconnect(NetworkAgent networkAgent) {
        if (networkAgent != null) {
            NetworkInfo networkInfo = new NetworkInfo(mNetworkInfo);
            networkInfo.setIsAvailable(false);
            networkInfo.setDetailedState(DetailedState.DISCONNECTED, null, null);
        if (networkAgent != 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);
+63 −26
Original line number Diff line number Diff line
@@ -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));

        // Block calls to the NotificationManager or PendingIntent#getActivity.
        doNothing().when(vpn).updateAlwaysOnNotificationInternal(anyBoolean());
        return vpn;
    }

        public void assertBlocked(int... uids) {
    private static void assertBlocked(Vpn vpn, int... uids) {
        for (int uid : uids) {
                assertTrue("Uid " + uid + " should be blocked", isBlockingUid(uid));
            assertTrue("Uid " + uid + " should be blocked", vpn.isBlockingUid(uid));
        }
    }

        public void assertUnblocked(int... uids) {
    private static void assertUnblocked(Vpn vpn, int... uids) {
        for (int uid : uids) {
                assertFalse("Uid " + uid + " should not be blocked", isBlockingUid(uid));
            }
            assertFalse("Uid " + uid + " should not be blocked", vpn.isBlockingUid(uid));
        }
    }