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