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