1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.settings.vpn2;
17 
18 import static android.app.AppOpsManager.OP_ACTIVATE_PLATFORM_VPN;
19 import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
20 
21 import android.annotation.NonNull;
22 import android.app.AppOpsManager;
23 import android.app.Dialog;
24 import android.app.admin.DevicePolicyManager;
25 import android.app.settings.SettingsEnums;
26 import android.content.Context;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.net.ConnectivityManager;
32 import android.net.IConnectivityManager;
33 import android.os.Bundle;
34 import android.os.RemoteException;
35 import android.os.ServiceManager;
36 import android.os.UserHandle;
37 import android.os.UserManager;
38 import android.text.TextUtils;
39 import android.util.Log;
40 
41 import androidx.annotation.VisibleForTesting;
42 import androidx.appcompat.app.AlertDialog;
43 import androidx.fragment.app.DialogFragment;
44 import androidx.preference.Preference;
45 
46 import com.android.internal.net.VpnConfig;
47 import com.android.internal.util.ArrayUtils;
48 import com.android.settings.R;
49 import com.android.settings.SettingsPreferenceFragment;
50 import com.android.settings.core.SubSettingLauncher;
51 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
52 import com.android.settingslib.RestrictedLockUtils;
53 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
54 import com.android.settingslib.RestrictedPreference;
55 import com.android.settingslib.RestrictedSwitchPreference;
56 
57 import java.util.List;
58 
59 public class AppManagementFragment extends SettingsPreferenceFragment
60         implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
61         ConfirmLockdownFragment.ConfirmLockdownListener {
62 
63     private static final String TAG = "AppManagementFragment";
64 
65     private static final String ARG_PACKAGE_NAME = "package";
66 
67     private static final String KEY_VERSION = "version";
68     private static final String KEY_ALWAYS_ON_VPN = "always_on_vpn";
69     private static final String KEY_LOCKDOWN_VPN = "lockdown_vpn";
70     private static final String KEY_FORGET_VPN = "forget_vpn";
71 
72     private PackageManager mPackageManager;
73     private DevicePolicyManager mDevicePolicyManager;
74     private ConnectivityManager mConnectivityManager;
75     private IConnectivityManager mConnectivityService;
76 
77     // VPN app info
78     private final int mUserId = UserHandle.myUserId();
79     private String mPackageName;
80     private PackageInfo mPackageInfo;
81     private String mVpnLabel;
82 
83     // UI preference
84     private Preference mPreferenceVersion;
85     private RestrictedSwitchPreference mPreferenceAlwaysOn;
86     private RestrictedSwitchPreference mPreferenceLockdown;
87     private RestrictedPreference mPreferenceForget;
88 
89     // Listener
90     private final AppDialogFragment.Listener mForgetVpnDialogFragmentListener =
91             new AppDialogFragment.Listener() {
92         @Override
93         public void onForget() {
94             // Unset always-on-vpn when forgetting the VPN
95             if (isVpnAlwaysOn()) {
96                 setAlwaysOnVpn(false, false);
97             }
98             // Also dismiss and go back to VPN list
99             finish();
100         }
101 
102         @Override
103         public void onCancel() {
104             // do nothing
105         }
106     };
107 
show(Context context, AppPreference pref, int sourceMetricsCategory)108     public static void show(Context context, AppPreference pref, int sourceMetricsCategory) {
109         final Bundle args = new Bundle();
110         args.putString(ARG_PACKAGE_NAME, pref.getPackageName());
111         new SubSettingLauncher(context)
112                 .setDestination(AppManagementFragment.class.getName())
113                 .setArguments(args)
114                 .setTitleText(pref.getLabel())
115                 .setSourceMetricsCategory(sourceMetricsCategory)
116                 .setUserHandle(new UserHandle(pref.getUserId()))
117                 .launch();
118     }
119 
120     @Override
onCreate(Bundle savedState)121     public void onCreate(Bundle savedState) {
122         super.onCreate(savedState);
123         addPreferencesFromResource(R.xml.vpn_app_management);
124 
125         mPackageManager = getContext().getPackageManager();
126         mDevicePolicyManager = getContext().getSystemService(DevicePolicyManager.class);
127         mConnectivityManager = getContext().getSystemService(ConnectivityManager.class);
128         mConnectivityService = IConnectivityManager.Stub
129                 .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
130 
131         mPreferenceVersion = findPreference(KEY_VERSION);
132         mPreferenceAlwaysOn = (RestrictedSwitchPreference) findPreference(KEY_ALWAYS_ON_VPN);
133         mPreferenceLockdown = (RestrictedSwitchPreference) findPreference(KEY_LOCKDOWN_VPN);
134         mPreferenceForget = (RestrictedPreference) findPreference(KEY_FORGET_VPN);
135 
136         mPreferenceAlwaysOn.setOnPreferenceChangeListener(this);
137         mPreferenceLockdown.setOnPreferenceChangeListener(this);
138         mPreferenceForget.setOnPreferenceClickListener(this);
139     }
140 
141     @Override
onResume()142     public void onResume() {
143         super.onResume();
144 
145         boolean isInfoLoaded = loadInfo();
146         if (isInfoLoaded) {
147             mPreferenceVersion.setTitle(
148                     getPrefContext().getString(R.string.vpn_version, mPackageInfo.versionName));
149             updateUI();
150         } else {
151             finish();
152         }
153     }
154 
155     @Override
onPreferenceClick(Preference preference)156     public boolean onPreferenceClick(Preference preference) {
157         String key = preference.getKey();
158         switch (key) {
159             case KEY_FORGET_VPN:
160                 return onForgetVpnClick();
161             default:
162                 Log.w(TAG, "unknown key is clicked: " + key);
163                 return false;
164         }
165     }
166 
167     @Override
onPreferenceChange(Preference preference, Object newValue)168     public boolean onPreferenceChange(Preference preference, Object newValue) {
169         switch (preference.getKey()) {
170             case KEY_ALWAYS_ON_VPN:
171                 return onAlwaysOnVpnClick((Boolean) newValue, mPreferenceLockdown.isChecked());
172             case KEY_LOCKDOWN_VPN:
173                 return onAlwaysOnVpnClick(mPreferenceAlwaysOn.isChecked(), (Boolean) newValue);
174             default:
175                 Log.w(TAG, "unknown key is clicked: " + preference.getKey());
176                 return false;
177         }
178     }
179 
180     @Override
getMetricsCategory()181     public int getMetricsCategory() {
182         return SettingsEnums.VPN;
183     }
184 
onForgetVpnClick()185     private boolean onForgetVpnClick() {
186         updateRestrictedViews();
187         if (!mPreferenceForget.isEnabled()) {
188             return false;
189         }
190         AppDialogFragment.show(this, mForgetVpnDialogFragmentListener, mPackageInfo, mVpnLabel,
191                 true /* editing */, true);
192         return true;
193     }
194 
onAlwaysOnVpnClick(final boolean alwaysOnSetting, final boolean lockdown)195     private boolean onAlwaysOnVpnClick(final boolean alwaysOnSetting, final boolean lockdown) {
196         final boolean replacing = isAnotherVpnActive();
197         final boolean wasLockdown = VpnUtils.isAnyLockdownActive(getActivity());
198         if (ConfirmLockdownFragment.shouldShow(replacing, wasLockdown, lockdown)) {
199             // Place a dialog to confirm that traffic should be locked down.
200             final Bundle options = null;
201             ConfirmLockdownFragment.show(
202                     this, replacing, alwaysOnSetting, wasLockdown, lockdown, options);
203             return false;
204         }
205         // No need to show the dialog. Change the setting straight away.
206         return setAlwaysOnVpnByUI(alwaysOnSetting, lockdown);
207     }
208 
209     @Override
onConfirmLockdown(Bundle options, boolean isEnabled, boolean isLockdown)210     public void onConfirmLockdown(Bundle options, boolean isEnabled, boolean isLockdown) {
211         setAlwaysOnVpnByUI(isEnabled, isLockdown);
212     }
213 
setAlwaysOnVpnByUI(boolean isEnabled, boolean isLockdown)214     private boolean setAlwaysOnVpnByUI(boolean isEnabled, boolean isLockdown) {
215         updateRestrictedViews();
216         if (!mPreferenceAlwaysOn.isEnabled()) {
217             return false;
218         }
219         // Only clear legacy lockdown vpn in system user.
220         if (mUserId == UserHandle.USER_SYSTEM) {
221             VpnUtils.clearLockdownVpn(getContext());
222         }
223         final boolean success = setAlwaysOnVpn(isEnabled, isLockdown);
224         if (isEnabled && (!success || !isVpnAlwaysOn())) {
225             CannotConnectFragment.show(this, mVpnLabel);
226         } else {
227             updateUI();
228         }
229         return success;
230     }
231 
setAlwaysOnVpn(boolean isEnabled, boolean isLockdown)232     private boolean setAlwaysOnVpn(boolean isEnabled, boolean isLockdown) {
233         return mConnectivityManager.setAlwaysOnVpnPackageForUser(mUserId,
234                 isEnabled ? mPackageName : null, isLockdown, /* lockdownWhitelist */ null);
235     }
236 
updateUI()237     private void updateUI() {
238         if (isAdded()) {
239             final boolean alwaysOn = isVpnAlwaysOn();
240             final boolean lockdown = alwaysOn
241                     && VpnUtils.isAnyLockdownActive(getActivity());
242 
243             mPreferenceAlwaysOn.setChecked(alwaysOn);
244             mPreferenceLockdown.setChecked(lockdown);
245             updateRestrictedViews();
246         }
247     }
248 
updateRestrictedViews()249     private void updateRestrictedViews() {
250         if (isAdded()) {
251             mPreferenceAlwaysOn.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN,
252                     mUserId);
253             mPreferenceLockdown.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN,
254                     mUserId);
255             mPreferenceForget.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN,
256                     mUserId);
257 
258             if (mPackageName.equals(mDevicePolicyManager.getAlwaysOnVpnPackage())) {
259                 EnforcedAdmin admin = RestrictedLockUtils.getProfileOrDeviceOwner(
260                         getContext(), UserHandle.of(mUserId));
261                 mPreferenceAlwaysOn.setDisabledByAdmin(admin);
262                 mPreferenceForget.setDisabledByAdmin(admin);
263                 if (mDevicePolicyManager.isAlwaysOnVpnLockdownEnabled()) {
264                     mPreferenceLockdown.setDisabledByAdmin(admin);
265                 }
266             }
267             if (mConnectivityManager.isAlwaysOnVpnPackageSupportedForUser(mUserId, mPackageName)) {
268                 // setSummary doesn't override the admin message when user restriction is applied
269                 mPreferenceAlwaysOn.setSummary(R.string.vpn_always_on_summary);
270                 // setEnabled is not required here, as checkRestrictionAndSetDisabled
271                 // should have refreshed the enable state.
272             } else {
273                 mPreferenceAlwaysOn.setEnabled(false);
274                 mPreferenceLockdown.setEnabled(false);
275                 mPreferenceAlwaysOn.setSummary(R.string.vpn_always_on_summary_not_supported);
276             }
277         }
278     }
279 
getAlwaysOnVpnPackage()280     private String getAlwaysOnVpnPackage() {
281         return mConnectivityManager.getAlwaysOnVpnPackageForUser(mUserId);
282     }
283 
isVpnAlwaysOn()284     private boolean isVpnAlwaysOn() {
285         return mPackageName.equals(getAlwaysOnVpnPackage());
286     }
287 
288     /**
289      * @return false if the intent doesn't contain an existing package or can't retrieve activated
290      * vpn info.
291      */
loadInfo()292     private boolean loadInfo() {
293         final Bundle args = getArguments();
294         if (args == null) {
295             Log.e(TAG, "empty bundle");
296             return false;
297         }
298 
299         mPackageName = args.getString(ARG_PACKAGE_NAME);
300         if (mPackageName == null) {
301             Log.e(TAG, "empty package name");
302             return false;
303         }
304 
305         try {
306             mPackageInfo = mPackageManager.getPackageInfo(mPackageName, /* PackageInfoFlags */ 0);
307             mVpnLabel = VpnConfig.getVpnLabel(getPrefContext(), mPackageName).toString();
308         } catch (NameNotFoundException nnfe) {
309             Log.e(TAG, "package not found", nnfe);
310             return false;
311         }
312 
313         if (mPackageInfo.applicationInfo == null) {
314             Log.e(TAG, "package does not include an application");
315             return false;
316         }
317         if (!appHasVpnPermission(getContext(), mPackageInfo.applicationInfo)) {
318             Log.e(TAG, "package didn't register VPN profile");
319             return false;
320         }
321 
322         return true;
323     }
324 
325     @VisibleForTesting
appHasVpnPermission(Context context, @NonNull ApplicationInfo application)326     static boolean appHasVpnPermission(Context context, @NonNull ApplicationInfo application) {
327         final AppOpsManager service =
328                 (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
329         final List<AppOpsManager.PackageOps> ops = service.getOpsForPackage(application.uid,
330                 application.packageName, new int[]{OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN});
331         return !ArrayUtils.isEmpty(ops);
332     }
333 
334     /**
335      * @return {@code true} if another VPN (VpnService or legacy) is connected or set as always-on.
336      */
isAnotherVpnActive()337     private boolean isAnotherVpnActive() {
338         try {
339             final VpnConfig config = mConnectivityService.getVpnConfig(mUserId);
340             return config != null && !TextUtils.equals(config.user, mPackageName);
341         } catch (RemoteException e) {
342             Log.w(TAG, "Failure to look up active VPN", e);
343             return false;
344         }
345     }
346 
347     public static class CannotConnectFragment extends InstrumentedDialogFragment {
348         private static final String TAG = "CannotConnect";
349         private static final String ARG_VPN_LABEL = "label";
350 
351         @Override
getMetricsCategory()352         public int getMetricsCategory() {
353             return SettingsEnums.DIALOG_VPN_CANNOT_CONNECT;
354         }
355 
show(AppManagementFragment parent, String vpnLabel)356         public static void show(AppManagementFragment parent, String vpnLabel) {
357             if (parent.getFragmentManager().findFragmentByTag(TAG) == null) {
358                 final Bundle args = new Bundle();
359                 args.putString(ARG_VPN_LABEL, vpnLabel);
360 
361                 final DialogFragment frag = new CannotConnectFragment();
362                 frag.setArguments(args);
363                 frag.show(parent.getFragmentManager(), TAG);
364             }
365         }
366 
367         @Override
onCreateDialog(Bundle savedInstanceState)368         public Dialog onCreateDialog(Bundle savedInstanceState) {
369             final String vpnLabel = getArguments().getString(ARG_VPN_LABEL);
370             return new AlertDialog.Builder(getActivity())
371                     .setTitle(getActivity().getString(R.string.vpn_cant_connect_title, vpnLabel))
372                     .setMessage(getActivity().getString(R.string.vpn_cant_connect_message))
373                     .setPositiveButton(R.string.okay, null)
374                     .create();
375         }
376     }
377 }
378