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