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