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