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