diff --git a/src/com/android/settings/network/MobileNetworkPreferenceController.java b/src/com/android/settings/network/MobileNetworkPreferenceController.java index f55e753ac36..c7abf905cd9 100644 --- a/src/com/android/settings/network/MobileNetworkPreferenceController.java +++ b/src/com/android/settings/network/MobileNetworkPreferenceController.java @@ -26,7 +26,6 @@ import android.telephony.TelephonyManager; import com.android.settings.Utils; import com.android.settings.core.PreferenceController; -import com.android.settings.core.lifecycle.Lifecycle; import com.android.settings.core.lifecycle.LifecycleObserver; import com.android.settings.core.lifecycle.events.OnPause; import com.android.settings.core.lifecycle.events.OnResume; diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java index 9a811b86dbb..f52230b61d0 100644 --- a/src/com/android/settings/network/NetworkDashboardFragment.java +++ b/src/com/android/settings/network/NetworkDashboardFragment.java @@ -83,17 +83,20 @@ public class NetworkDashboardFragment extends DashboardFragment implements new WifiMasterSwitchPreferenceController(context, mMetricsFeatureProvider); final MobileNetworkPreferenceController mobileNetworkPreferenceController = new MobileNetworkPreferenceController(context); + final VpnPreferenceController vpnPreferenceController = + new VpnPreferenceController(context); final Lifecycle lifecycle = getLifecycle(); lifecycle.addObserver(airplaneModePreferenceController); lifecycle.addObserver(mobilePlanPreferenceController); lifecycle.addObserver(wifiPreferenceController); lifecycle.addObserver(mobileNetworkPreferenceController); + lifecycle.addObserver(vpnPreferenceController); final List controllers = new ArrayList<>(); controllers.add(airplaneModePreferenceController); controllers.add(mobileNetworkPreferenceController); controllers.add(new TetherPreferenceController(context)); - controllers.add(new VpnPreferenceController(context)); + controllers.add(vpnPreferenceController); controllers.add(new ProxyPreferenceController(context)); controllers.add(mobilePlanPreferenceController); controllers.add(wifiPreferenceController); diff --git a/src/com/android/settings/network/VpnPreferenceController.java b/src/com/android/settings/network/VpnPreferenceController.java index f7e230ffd05..86ff175412d 100644 --- a/src/com/android/settings/network/VpnPreferenceController.java +++ b/src/com/android/settings/network/VpnPreferenceController.java @@ -16,38 +16,74 @@ package com.android.settings.network; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.net.ConnectivityManager; +import android.net.IConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; +import android.util.Log; +import android.util.SparseArray; +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.settings.R; import com.android.settings.core.PreferenceController; +import com.android.settings.core.lifecycle.LifecycleObserver; +import com.android.settings.core.lifecycle.events.OnPause; +import com.android.settings.core.lifecycle.events.OnResume; import com.android.settingslib.RestrictedLockUtils; +import java.util.List; -public class VpnPreferenceController extends PreferenceController { + +public class VpnPreferenceController extends PreferenceController implements LifecycleObserver, + OnResume, OnPause { private static final String KEY_VPN_SETTINGS = "vpn_settings"; + private static final NetworkRequest REQUEST = new NetworkRequest.Builder() + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + .build(); + private static final String TAG = "VpnPreferenceController"; private final String mToggleable; - private final boolean mIsSecondaryUser; + private final UserManager mUserManager; + private final ConnectivityManager mConnectivityManager; + private final IConnectivityManager mConnectivityManagerService; + private Preference mPreference; public VpnPreferenceController(Context context) { super(context); mToggleable = Settings.Global.getString(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); - mIsSecondaryUser = !UserManager.get(context).isAdminUser(); + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + mConnectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + mConnectivityManagerService = IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); + mPreference = screen.findPreference(KEY_VPN_SETTINGS); // Manually set dependencies for Wifi when not toggleable. if (mToggleable == null || !mToggleable.contains(Settings.Global.RADIO_WIFI)) { - final Preference pref = screen.findPreference(KEY_VPN_SETTINGS); - if (pref != null) { - pref.setDependency(AirplaneModePreferenceController.KEY_TOGGLE_AIRPLANE); + if (mPreference != null) { + mPreference.setDependency(AirplaneModePreferenceController.KEY_TOGGLE_AIRPLANE); } } } @@ -62,4 +98,96 @@ public class VpnPreferenceController extends PreferenceController { public String getPreferenceKey() { return KEY_VPN_SETTINGS; } + + @Override + public void onPause() { + if (isAvailable()) { + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + } + } + + @Override + public void onResume() { + if (isAvailable()) { + mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback); + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + void updateSummary() { + if (mPreference == null) { + return; + } + // Copied from SystemUI::SecurityControllerImpl + SparseArray vpns = new SparseArray<>(); + try { + final List users = mUserManager.getUsers(); + for (UserInfo user : users) { + VpnConfig cfg = mConnectivityManagerService.getVpnConfig(user.id); + if (cfg == null) { + continue; + } else if (cfg.legacy) { + // Legacy VPNs should do nothing if the network is disconnected. Third-party + // VPN warnings need to continue as traffic can still go to the app. + final LegacyVpnInfo legacyVpn = + mConnectivityManagerService.getLegacyVpnInfo(user.id); + if (legacyVpn == null || legacyVpn.state != LegacyVpnInfo.STATE_CONNECTED) { + continue; + } + } + vpns.put(user.id, cfg); + } + } catch (RemoteException rme) { + // Roll back to previous state + Log.e(TAG, "Unable to list active VPNs", rme); + return; + } + final UserInfo userInfo = mUserManager.getUserInfo(UserHandle.myUserId()); + final int uid; + if (userInfo.isRestricted()) { + uid = userInfo.restrictedProfileParentId; + } else { + uid = userInfo.id; + } + VpnConfig vpn = vpns.get(uid); + final String vpnName; + if (vpn == null) { + vpnName = null; + } else { + vpnName = getNameForVpnConfig(vpn, UserHandle.of(uid)); + } + new Handler(Looper.getMainLooper()).post(() -> mPreference.setSummary(vpnName)); + } + + private String getNameForVpnConfig(VpnConfig cfg, UserHandle user) { + if (cfg.legacy) { + return mContext.getString(R.string.bluetooth_connected); + } + // The package name for an active VPN is stored in the 'user' field of its VpnConfig + final String vpnPackage = cfg.user; + try { + Context userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), + 0 /* flags */, user); + return VpnConfig.getVpnLabel(userContext, vpnPackage).toString(); + } catch (PackageManager.NameNotFoundException nnfe) { + Log.e(TAG, "Package " + vpnPackage + " is not present", nnfe); + return null; + } + } + + // Copied from SystemUI::SecurityControllerImpl + private final ConnectivityManager.NetworkCallback + mNetworkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + Log.d(TAG, "onAvailable " + network.netId); + updateSummary(); + } + + @Override + public void onLost(Network network) { + Log.d(TAG, "onLost " + network.netId); + updateSummary(); + } + }; } diff --git a/tests/robotests/src/com/android/settings/network/VpnPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/VpnPreferenceControllerTest.java new file mode 100644 index 00000000000..2a0b873a726 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/VpnPreferenceControllerTest.java @@ -0,0 +1,103 @@ +/* + * 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.settings.network; + + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.IConnectivityManager; +import android.net.NetworkRequest; +import android.os.IBinder; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowServiceManager; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class VpnPreferenceControllerTest { + + @Mock + private Context mContext; + @Mock + private ConnectivityManager mConnectivityManager; + @Mock + private IBinder mBinder; + @Mock + private IConnectivityManager mConnectivityManagerService; + @Mock + private PreferenceScreen mScreen; + @Mock + private Preference mPreference; + private VpnPreferenceController mController; + private Lifecycle mLifecycle; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)) + .thenReturn(mConnectivityManager); + when(mBinder.queryLocalInterface("android.net.IConnectivityManager")) + .thenReturn(mConnectivityManagerService); + ShadowServiceManager.addService(Context.CONNECTIVITY_SERVICE, mBinder); + when(mScreen.findPreference(anyString())).thenReturn(mPreference); + + mController = spy(new VpnPreferenceController(mContext)); + mLifecycle = new Lifecycle(); + mLifecycle.addObserver(mController); + } + + @Test + public void displayPreference_available_shouldSetDependency() { + + doReturn(true).when(mController).isAvailable(); + mController.displayPreference(mScreen); + + verify(mPreference).setDependency(AirplaneModePreferenceController.KEY_TOGGLE_AIRPLANE); + } + + @Test + public void goThroughLifecycle_shouldRegisterUnregisterListener() { + doReturn(true).when(mController).isAvailable(); + + mLifecycle.onResume(); + verify(mConnectivityManager).registerNetworkCallback( + any(NetworkRequest.class), any(ConnectivityManager.NetworkCallback.class)); + + mLifecycle.onPause(); + verify(mConnectivityManager).unregisterNetworkCallback( + any(ConnectivityManager.NetworkCallback.class)); + } + +}