1/*
2 * Copyright (C) 2015 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
17package com.android.settings.vpn2;
18
19import android.app.AlertDialog;
20import android.app.Dialog;
21import android.app.DialogFragment;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.net.ConnectivityManager;
25import android.net.IConnectivityManager;
26import android.os.Bundle;
27import android.os.RemoteException;
28import android.os.ServiceManager;
29import android.os.UserHandle;
30import android.security.Credentials;
31import android.security.KeyStore;
32import android.util.Log;
33import android.view.View;
34import android.widget.Toast;
35
36import com.android.internal.logging.nano.MetricsProto;
37import com.android.internal.net.LegacyVpnInfo;
38import com.android.internal.net.VpnConfig;
39import com.android.internal.net.VpnProfile;
40import com.android.settings.R;
41import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
42
43/**
44 * Fragment wrapper around a {@link ConfigDialog}.
45 */
46public class ConfigDialogFragment extends InstrumentedDialogFragment implements
47        DialogInterface.OnClickListener, DialogInterface.OnShowListener, View.OnClickListener,
48        ConfirmLockdownFragment.ConfirmLockdownListener {
49    private static final String TAG_CONFIG_DIALOG = "vpnconfigdialog";
50    private static final String TAG = "ConfigDialogFragment";
51
52    private static final String ARG_PROFILE = "profile";
53    private static final String ARG_EDITING = "editing";
54    private static final String ARG_EXISTS = "exists";
55
56    private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface(
57            ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
58    private Context mContext;
59
60    private boolean mUnlocking = false;
61
62
63    @Override
64    public int getMetricsCategory() {
65        return MetricsProto.MetricsEvent.DIALOG_LEGACY_VPN_CONFIG;
66    }
67
68    public static void show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists) {
69        if (!parent.isAdded()) return;
70
71        Bundle args = new Bundle();
72        args.putParcelable(ARG_PROFILE, profile);
73        args.putBoolean(ARG_EDITING, edit);
74        args.putBoolean(ARG_EXISTS, exists);
75
76        final ConfigDialogFragment frag = new ConfigDialogFragment();
77        frag.setArguments(args);
78        frag.setTargetFragment(parent, 0);
79        frag.show(parent.getFragmentManager(), TAG_CONFIG_DIALOG);
80    }
81
82    @Override
83    public void onAttach(final Context context) {
84        super.onAttach(context);
85        mContext = context;
86    }
87
88    @Override
89    public void onResume() {
90        super.onResume();
91
92        // Check KeyStore here, so others do not need to deal with it.
93        if (!KeyStore.getInstance().isUnlocked()) {
94            if (!mUnlocking) {
95                // Let us unlock KeyStore. See you later!
96                Credentials.getInstance().unlock(mContext);
97            } else {
98                // We already tried, but it is still not working!
99                dismiss();
100            }
101            mUnlocking = !mUnlocking;
102            return;
103        }
104
105        // Now KeyStore is always unlocked. Reset the flag.
106        mUnlocking = false;
107    }
108
109    @Override
110    public Dialog onCreateDialog(Bundle savedInstanceState) {
111        Bundle args = getArguments();
112        VpnProfile profile = (VpnProfile) args.getParcelable(ARG_PROFILE);
113        boolean editing = args.getBoolean(ARG_EDITING);
114        boolean exists = args.getBoolean(ARG_EXISTS);
115
116        final Dialog dialog = new ConfigDialog(getActivity(), this, profile, editing, exists);
117        dialog.setOnShowListener(this);
118        return dialog;
119    }
120
121    /**
122     * Override for the default onClick handler which also calls dismiss().
123     *
124     * @see DialogInterface.OnClickListener#onClick(DialogInterface, int)
125     */
126    @Override
127    public void onShow(DialogInterface dialogInterface) {
128        ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
129    }
130
131    @Override
132    public void onClick(View positiveButton) {
133        onClick(getDialog(), AlertDialog.BUTTON_POSITIVE);
134    }
135
136    @Override
137    public void onConfirmLockdown(Bundle options, boolean isAlwaysOn, boolean isLockdown) {
138        VpnProfile profile = (VpnProfile) options.getParcelable(ARG_PROFILE);
139        connect(profile, isAlwaysOn);
140        dismiss();
141    }
142
143    @Override
144    public void onClick(DialogInterface dialogInterface, int button) {
145        ConfigDialog dialog = (ConfigDialog) getDialog();
146        VpnProfile profile = dialog.getProfile();
147
148        if (button == DialogInterface.BUTTON_POSITIVE) {
149            // Possibly throw up a dialog to explain lockdown VPN.
150            final boolean shouldLockdown = dialog.isVpnAlwaysOn();
151            final boolean shouldConnect = shouldLockdown || !dialog.isEditing();
152            final boolean wasLockdown = VpnUtils.isAnyLockdownActive(mContext);
153            try {
154                final boolean replace = VpnUtils.isVpnActive(mContext);
155                if (shouldConnect && !isConnected(profile) &&
156                        ConfirmLockdownFragment.shouldShow(replace, wasLockdown, shouldLockdown)) {
157                    final Bundle opts = new Bundle();
158                    opts.putParcelable(ARG_PROFILE, profile);
159                    ConfirmLockdownFragment.show(this, replace, /* alwaysOn */ shouldLockdown,
160                           /* from */  wasLockdown, /* to */ shouldLockdown, opts);
161                } else if (shouldConnect) {
162                    connect(profile, shouldLockdown);
163                } else {
164                    save(profile, false);
165                }
166            } catch (RemoteException e) {
167                Log.w(TAG, "Failed to check active VPN state. Skipping.", e);
168            }
169        } else if (button == DialogInterface.BUTTON_NEUTRAL) {
170            // Disable profile if connected
171            if (!disconnect(profile)) {
172                Log.e(TAG, "Failed to disconnect VPN. Leaving profile in keystore.");
173                return;
174            }
175
176            // Delete from KeyStore
177            KeyStore keyStore = KeyStore.getInstance();
178            keyStore.delete(Credentials.VPN + profile.key, KeyStore.UID_SELF);
179
180            updateLockdownVpn(false, profile);
181        }
182        dismiss();
183    }
184
185    @Override
186    public void onCancel(DialogInterface dialog) {
187        dismiss();
188        super.onCancel(dialog);
189    }
190
191    private void updateLockdownVpn(boolean isVpnAlwaysOn, VpnProfile profile) {
192        // Save lockdown vpn
193        if (isVpnAlwaysOn) {
194            // Show toast if vpn profile is not valid
195            if (!profile.isValidLockdownProfile()) {
196                Toast.makeText(mContext, R.string.vpn_lockdown_config_error,
197                        Toast.LENGTH_LONG).show();
198                return;
199            }
200
201            final ConnectivityManager conn = ConnectivityManager.from(mContext);
202            conn.setAlwaysOnVpnPackageForUser(UserHandle.myUserId(), null,
203                    /* lockdownEnabled */ false);
204            VpnUtils.setLockdownVpn(mContext, profile.key);
205        } else {
206            // update only if lockdown vpn has been changed
207            if (VpnUtils.isVpnLockdown(profile.key)) {
208                VpnUtils.clearLockdownVpn(mContext);
209            }
210        }
211    }
212
213    private void save(VpnProfile profile, boolean lockdown) {
214        KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(),
215                KeyStore.UID_SELF, /* flags */ 0);
216
217        // Flush out old version of profile
218        disconnect(profile);
219
220        // Notify lockdown VPN that the profile has changed.
221        updateLockdownVpn(lockdown, profile);
222    }
223
224    private void connect(VpnProfile profile, boolean lockdown) {
225        save(profile, lockdown);
226
227        // Now try to start the VPN - this is not necessary if the profile is set as lockdown,
228        // because just saving the profile in this mode will start a connection.
229        if (!VpnUtils.isVpnLockdown(profile.key)) {
230            VpnUtils.clearLockdownVpn(mContext);
231            try {
232                mService.startLegacyVpn(profile);
233            } catch (IllegalStateException e) {
234                Toast.makeText(mContext, R.string.vpn_no_network, Toast.LENGTH_LONG).show();
235            } catch (RemoteException e) {
236                Log.e(TAG, "Failed to connect", e);
237            }
238        }
239    }
240
241    /**
242     * Ensure that the VPN profile pointed at by {@param profile} is disconnected.
243     *
244     * @return {@code true} iff this VPN profile is no longer connected. Note that another profile
245     *         may still be active - this function will then do nothing but still return success.
246     */
247    private boolean disconnect(VpnProfile profile) {
248        try {
249            if (!isConnected(profile)) {
250                return true;
251            }
252            return VpnUtils.disconnectLegacyVpn(getContext());
253        } catch (RemoteException e) {
254            Log.e(TAG, "Failed to disconnect", e);
255            return false;
256        }
257    }
258
259    private boolean isConnected(VpnProfile profile) throws RemoteException {
260        LegacyVpnInfo connected = mService.getLegacyVpnInfo(UserHandle.myUserId());
261        return connected != null && profile.key.equals(connected.key);
262    }
263}
264