1/*
2 * Copyright (C) 2011 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.content.res.Resources;
25import android.net.ConnectivityManager;
26import android.net.IConnectivityManager;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.Message;
30import android.os.ServiceManager;
31import android.os.SystemProperties;
32import android.os.UserManager;
33import android.preference.Preference;
34import android.preference.PreferenceGroup;
35import android.preference.PreferenceScreen;
36import android.security.Credentials;
37import android.security.KeyStore;
38import android.text.TextUtils;
39import android.util.Log;
40import android.view.ContextMenu;
41import android.view.ContextMenu.ContextMenuInfo;
42import android.view.LayoutInflater;
43import android.view.Menu;
44import android.view.MenuInflater;
45import android.view.MenuItem;
46import android.view.View;
47import android.widget.AdapterView.AdapterContextMenuInfo;
48import android.widget.AdapterView.OnItemSelectedListener;
49import android.widget.ArrayAdapter;
50import android.widget.ListView;
51import android.widget.Spinner;
52import android.widget.TextView;
53import android.widget.Toast;
54
55import com.android.internal.net.LegacyVpnInfo;
56import com.android.internal.net.VpnConfig;
57import com.android.internal.net.VpnProfile;
58import com.android.internal.util.ArrayUtils;
59import com.android.settings.R;
60import com.android.settings.SettingsPreferenceFragment;
61import com.google.android.collect.Lists;
62
63import java.util.ArrayList;
64import java.util.HashMap;
65import java.util.List;
66
67public class VpnSettings extends SettingsPreferenceFragment implements
68        Handler.Callback, Preference.OnPreferenceClickListener,
69        DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
70    private static final String TAG = "VpnSettings";
71
72    private static final String TAG_LOCKDOWN = "lockdown";
73
74    private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN";
75
76    // TODO: migrate to using DialogFragment when editing
77
78    private final IConnectivityManager mService = IConnectivityManager.Stub
79            .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
80    private final KeyStore mKeyStore = KeyStore.getInstance();
81    private boolean mUnlocking = false;
82
83    private HashMap<String, VpnPreference> mPreferences = new HashMap<String, VpnPreference>();
84    private VpnDialog mDialog;
85
86    private Handler mUpdater;
87    private LegacyVpnInfo mInfo;
88    private UserManager mUm;
89
90    // The key of the profile for the current ContextMenu.
91    private String mSelectedKey;
92
93    private boolean mUnavailable;
94
95    @Override
96    public void onCreate(Bundle savedState) {
97        super.onCreate(savedState);
98
99        mUm = (UserManager) getSystemService(Context.USER_SERVICE);
100
101        if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) {
102            mUnavailable = true;
103            setPreferenceScreen(new PreferenceScreen(getActivity(), null));
104            return;
105        }
106
107        setHasOptionsMenu(true);
108        addPreferencesFromResource(R.xml.vpn_settings2);
109
110        if (savedState != null) {
111            VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"),
112                    savedState.getByteArray("VpnProfile"));
113            if (profile != null) {
114                mDialog = new VpnDialog(getActivity(), this, profile,
115                        savedState.getBoolean("VpnEditing"));
116            }
117        }
118    }
119
120    @Override
121    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
122        super.onCreateOptionsMenu(menu, inflater);
123        inflater.inflate(R.menu.vpn, menu);
124    }
125
126    @Override
127    public void onPrepareOptionsMenu(Menu menu) {
128        super.onPrepareOptionsMenu(menu);
129
130        // Hide lockdown VPN on devices that require IMS authentication
131        if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
132            menu.findItem(R.id.vpn_lockdown).setVisible(false);
133        }
134    }
135
136    @Override
137    public boolean onOptionsItemSelected(MenuItem item) {
138        switch (item.getItemId()) {
139            case R.id.vpn_create: {
140                // Generate a new key. Here we just use the current time.
141                long millis = System.currentTimeMillis();
142                while (mPreferences.containsKey(Long.toHexString(millis))) {
143                    ++millis;
144                }
145                mDialog = new VpnDialog(
146                        getActivity(), this, new VpnProfile(Long.toHexString(millis)), true);
147                mDialog.setOnDismissListener(this);
148                mDialog.show();
149                return true;
150            }
151            case R.id.vpn_lockdown: {
152                LockdownConfigFragment.show(this);
153                return true;
154            }
155        }
156        return super.onOptionsItemSelected(item);
157    }
158
159    @Override
160    public void onSaveInstanceState(Bundle savedState) {
161        // We do not save view hierarchy, as they are just profiles.
162        if (mDialog != null) {
163            VpnProfile profile = mDialog.getProfile();
164            savedState.putString("VpnKey", profile.key);
165            savedState.putByteArray("VpnProfile", profile.encode());
166            savedState.putBoolean("VpnEditing", mDialog.isEditing());
167        }
168        // else?
169    }
170
171    @Override
172    public void onResume() {
173        super.onResume();
174
175        if (mUnavailable) {
176            TextView emptyView = (TextView) getView().findViewById(android.R.id.empty);
177            getListView().setEmptyView(emptyView);
178            if (emptyView != null) {
179                emptyView.setText(R.string.vpn_settings_not_available);
180            }
181            return;
182        }
183
184        final boolean pickLockdown = getActivity()
185                .getIntent().getBooleanExtra(EXTRA_PICK_LOCKDOWN, false);
186        if (pickLockdown) {
187            LockdownConfigFragment.show(this);
188        }
189
190        // Check KeyStore here, so others do not need to deal with it.
191        if (!mKeyStore.isUnlocked()) {
192            if (!mUnlocking) {
193                // Let us unlock KeyStore. See you later!
194                Credentials.getInstance().unlock(getActivity());
195            } else {
196                // We already tried, but it is still not working!
197                finishFragment();
198            }
199            mUnlocking = !mUnlocking;
200            return;
201        }
202
203        // Now KeyStore is always unlocked. Reset the flag.
204        mUnlocking = false;
205
206        // Currently we are the only user of profiles in KeyStore.
207        // Assuming KeyStore and KeyGuard do the right thing, we can
208        // safely cache profiles in the memory.
209        if (mPreferences.size() == 0) {
210            PreferenceGroup group = getPreferenceScreen();
211
212            final Context context = getActivity();
213            final List<VpnProfile> profiles = loadVpnProfiles(mKeyStore);
214            for (VpnProfile profile : profiles) {
215                final VpnPreference pref = new VpnPreference(context, profile);
216                pref.setOnPreferenceClickListener(this);
217                mPreferences.put(profile.key, pref);
218                group.addPreference(pref);
219            }
220        }
221
222        // Show the dialog if there is one.
223        if (mDialog != null) {
224            mDialog.setOnDismissListener(this);
225            mDialog.show();
226        }
227
228        // Start monitoring.
229        if (mUpdater == null) {
230            mUpdater = new Handler(this);
231        }
232        mUpdater.sendEmptyMessage(0);
233
234        // Register for context menu. Hmmm, getListView() is hidden?
235        registerForContextMenu(getListView());
236    }
237
238    @Override
239    public void onPause() {
240        super.onPause();
241
242        if (mUnavailable) {
243            return;
244        }
245
246        // Hide the dialog if there is one.
247        if (mDialog != null) {
248            mDialog.setOnDismissListener(null);
249            mDialog.dismiss();
250        }
251
252        // Unregister for context menu.
253        if (getView() != null) {
254            unregisterForContextMenu(getListView());
255        }
256    }
257
258    @Override
259    public void onDismiss(DialogInterface dialog) {
260        // Here is the exit of a dialog.
261        mDialog = null;
262    }
263
264    @Override
265    public void onClick(DialogInterface dialog, int button) {
266        if (button == DialogInterface.BUTTON_POSITIVE) {
267            // Always save the profile.
268            VpnProfile profile = mDialog.getProfile();
269            mKeyStore.put(Credentials.VPN + profile.key, profile.encode(), KeyStore.UID_SELF,
270                    KeyStore.FLAG_ENCRYPTED);
271
272            // Update the preference.
273            VpnPreference preference = mPreferences.get(profile.key);
274            if (preference != null) {
275                disconnect(profile.key);
276                preference.update(profile);
277            } else {
278                preference = new VpnPreference(getActivity(), profile);
279                preference.setOnPreferenceClickListener(this);
280                mPreferences.put(profile.key, preference);
281                getPreferenceScreen().addPreference(preference);
282            }
283
284            // If we are not editing, connect!
285            if (!mDialog.isEditing()) {
286                try {
287                    connect(profile);
288                } catch (Exception e) {
289                    Log.e(TAG, "connect", e);
290                }
291            }
292        }
293    }
294
295    @Override
296    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
297        if (mDialog != null) {
298            Log.v(TAG, "onCreateContextMenu() is called when mDialog != null");
299            return;
300        }
301
302        if (info instanceof AdapterContextMenuInfo) {
303            Preference preference = (Preference) getListView().getItemAtPosition(
304                    ((AdapterContextMenuInfo) info).position);
305            if (preference instanceof VpnPreference) {
306                VpnProfile profile = ((VpnPreference) preference).getProfile();
307                mSelectedKey = profile.key;
308                menu.setHeaderTitle(profile.name);
309                menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit);
310                menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete);
311            }
312        }
313    }
314
315    @Override
316    public boolean onContextItemSelected(MenuItem item) {
317        if (mDialog != null) {
318            Log.v(TAG, "onContextItemSelected() is called when mDialog != null");
319            return false;
320        }
321
322        VpnPreference preference = mPreferences.get(mSelectedKey);
323        if (preference == null) {
324            Log.v(TAG, "onContextItemSelected() is called but no preference is found");
325            return false;
326        }
327
328        switch (item.getItemId()) {
329            case R.string.vpn_menu_edit:
330                mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true);
331                mDialog.setOnDismissListener(this);
332                mDialog.show();
333                return true;
334            case R.string.vpn_menu_delete:
335                disconnect(mSelectedKey);
336                getPreferenceScreen().removePreference(preference);
337                mPreferences.remove(mSelectedKey);
338                mKeyStore.delete(Credentials.VPN + mSelectedKey);
339                return true;
340        }
341        return false;
342    }
343
344    @Override
345    public boolean onPreferenceClick(Preference preference) {
346        if (mDialog != null) {
347            Log.v(TAG, "onPreferenceClick() is called when mDialog != null");
348            return true;
349        }
350
351        if (preference instanceof VpnPreference) {
352            VpnProfile profile = ((VpnPreference) preference).getProfile();
353            if (mInfo != null && profile.key.equals(mInfo.key) &&
354                    mInfo.state == LegacyVpnInfo.STATE_CONNECTED) {
355                try {
356                    mInfo.intent.send();
357                    return true;
358                } catch (Exception e) {
359                    // ignore
360                }
361            }
362            mDialog = new VpnDialog(getActivity(), this, profile, false);
363        } else {
364            // Generate a new key. Here we just use the current time.
365            long millis = System.currentTimeMillis();
366            while (mPreferences.containsKey(Long.toHexString(millis))) {
367                ++millis;
368            }
369            mDialog = new VpnDialog(getActivity(), this,
370                    new VpnProfile(Long.toHexString(millis)), true);
371        }
372        mDialog.setOnDismissListener(this);
373        mDialog.show();
374        return true;
375    }
376
377    @Override
378    public boolean handleMessage(Message message) {
379        mUpdater.removeMessages(0);
380
381        if (isResumed()) {
382            try {
383                LegacyVpnInfo info = mService.getLegacyVpnInfo();
384                if (mInfo != null) {
385                    VpnPreference preference = mPreferences.get(mInfo.key);
386                    if (preference != null) {
387                        preference.update(-1);
388                    }
389                    mInfo = null;
390                }
391                if (info != null) {
392                    VpnPreference preference = mPreferences.get(info.key);
393                    if (preference != null) {
394                        preference.update(info.state);
395                        mInfo = info;
396                    }
397                }
398            } catch (Exception e) {
399                // ignore
400            }
401            mUpdater.sendEmptyMessageDelayed(0, 1000);
402        }
403        return true;
404    }
405
406    private void connect(VpnProfile profile) throws Exception {
407        try {
408            mService.startLegacyVpn(profile);
409        } catch (IllegalStateException e) {
410            Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show();
411        }
412    }
413
414    private void disconnect(String key) {
415        if (mInfo != null && key.equals(mInfo.key)) {
416            try {
417                mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
418            } catch (Exception e) {
419                // ignore
420            }
421        }
422    }
423
424    @Override
425    protected int getHelpResource() {
426        return R.string.help_url_vpn;
427    }
428
429    private static class VpnPreference extends Preference {
430        private VpnProfile mProfile;
431        private int mState = -1;
432
433        VpnPreference(Context context, VpnProfile profile) {
434            super(context);
435            setPersistent(false);
436            setOrder(0);
437
438            mProfile = profile;
439            update();
440        }
441
442        VpnProfile getProfile() {
443            return mProfile;
444        }
445
446        void update(VpnProfile profile) {
447            mProfile = profile;
448            update();
449        }
450
451        void update(int state) {
452            mState = state;
453            update();
454        }
455
456        void update() {
457            if (mState < 0) {
458                String[] types = getContext().getResources()
459                        .getStringArray(R.array.vpn_types_long);
460                setSummary(types[mProfile.type]);
461            } else {
462                String[] states = getContext().getResources()
463                        .getStringArray(R.array.vpn_states);
464                setSummary(states[mState]);
465            }
466            setTitle(mProfile.name);
467            notifyHierarchyChanged();
468        }
469
470        @Override
471        public int compareTo(Preference preference) {
472            int result = -1;
473            if (preference instanceof VpnPreference) {
474                VpnPreference another = (VpnPreference) preference;
475                if ((result = another.mState - mState) == 0 &&
476                        (result = mProfile.name.compareTo(another.mProfile.name)) == 0 &&
477                        (result = mProfile.type - another.mProfile.type) == 0) {
478                    result = mProfile.key.compareTo(another.mProfile.key);
479                }
480            }
481            return result;
482        }
483    }
484
485    /**
486     * Dialog to configure always-on VPN.
487     */
488    public static class LockdownConfigFragment extends DialogFragment {
489        private List<VpnProfile> mProfiles;
490        private List<CharSequence> mTitles;
491        private int mCurrentIndex;
492
493        private static class TitleAdapter extends ArrayAdapter<CharSequence> {
494            public TitleAdapter(Context context, List<CharSequence> objects) {
495                super(context, com.android.internal.R.layout.select_dialog_singlechoice_material,
496                        android.R.id.text1, objects);
497            }
498        }
499
500        public static void show(VpnSettings parent) {
501            if (!parent.isAdded()) return;
502
503            final LockdownConfigFragment dialog = new LockdownConfigFragment();
504            dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN);
505        }
506
507        private static String getStringOrNull(KeyStore keyStore, String key) {
508            final byte[] value = keyStore.get(Credentials.LOCKDOWN_VPN);
509            return value == null ? null : new String(value);
510        }
511
512        private void initProfiles(KeyStore keyStore, Resources res) {
513            final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN);
514
515            mProfiles = loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP);
516            mTitles = Lists.newArrayList();
517            mTitles.add(res.getText(R.string.vpn_lockdown_none));
518            mCurrentIndex = 0;
519
520            for (VpnProfile profile : mProfiles) {
521                if (TextUtils.equals(profile.key, lockdownKey)) {
522                    mCurrentIndex = mTitles.size();
523                }
524                mTitles.add(profile.name);
525            }
526        }
527
528        @Override
529        public Dialog onCreateDialog(Bundle savedInstanceState) {
530            final Context context = getActivity();
531            final KeyStore keyStore = KeyStore.getInstance();
532
533            initProfiles(keyStore, context.getResources());
534
535            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
536            final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
537
538            builder.setTitle(R.string.vpn_menu_lockdown);
539
540            final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false);
541            final ListView listView = (ListView) view.findViewById(android.R.id.list);
542            listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
543            listView.setAdapter(new TitleAdapter(context, mTitles));
544            listView.setItemChecked(mCurrentIndex, true);
545            builder.setView(view);
546
547            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
548                @Override
549                public void onClick(DialogInterface dialog, int which) {
550                    final int newIndex = listView.getCheckedItemPosition();
551                    if (mCurrentIndex == newIndex) return;
552
553                    if (newIndex == 0) {
554                        keyStore.delete(Credentials.LOCKDOWN_VPN);
555
556                    } else {
557                        final VpnProfile profile = mProfiles.get(newIndex - 1);
558                        if (!profile.isValidLockdownProfile()) {
559                            Toast.makeText(context, R.string.vpn_lockdown_config_error,
560                                    Toast.LENGTH_LONG).show();
561                            return;
562                        }
563                        keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes(),
564                                KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED);
565                    }
566
567                    // kick profiles since we changed them
568                    ConnectivityManager.from(getActivity()).updateLockdownVpn();
569                }
570            });
571
572            return builder.create();
573        }
574    }
575
576    private static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
577        final ArrayList<VpnProfile> result = Lists.newArrayList();
578        final String[] keys = keyStore.saw(Credentials.VPN);
579        if (keys != null) {
580            for (String key : keys) {
581                final VpnProfile profile = VpnProfile.decode(
582                        key, keyStore.get(Credentials.VPN + key));
583                if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {
584                    result.add(profile);
585                }
586            }
587        }
588        return result;
589    }
590}
591