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