Summary:
frameworks/base
keystore rewrite
keyguard integration with keystore on keyguard entry or keyguard change
KeyStore API simplification
packages/apps/Settings
Removed com.android.credentials.SET_PASSWORD intent support
Added keyguard requirement for keystore use
packages/apps/CertInstaller
Tracking KeyStore API changes
Fix for NPE in CertInstaller when certificate lacks basic constraints
packages/apps/KeyChain
Tracking KeyStore API changes
Details:
frameworks/base
Move keystore from C to C++ while rewriting password
implementation. Removed global variables. Added many comments.
cmds/keystore/Android.mk
cmds/keystore/keystore.h
cmds/keystore/keystore.c => cmds/keystore/keystore.cpp
cmds/keystore/keystore_cli.c => cmds/keystore/keystore_cli.cpp
Changed saveLockPattern and saveLockPassword to notify the keystore
on changes so that the keystore master key can be reencrypted when
the keyguard changes.
core/java/com/android/internal/widget/LockPatternUtils.java
Changed unlock screens to pass values for keystore unlock or initialization
policy/src/com/android/internal/policy/impl/PasswordUnlockScreen.java
policy/src/com/android/internal/policy/impl/PatternUnlockScreen.java
KeyStore API changes
- renamed test() to state(), which now return a State enum
- made APIs with byte[] key arguments private
- added new KeyStore.isEmpty used to determine if a keyguard is required
keystore/java/android/security/KeyStore.java
In addition to tracking KeyStore API changes, added new testIsEmpty
and improved some existing tests to validate expect values.
keystore/tests/src/android/security/KeyStoreTest.java
packages/apps/Settings
Removing com.android.credentials.SET_PASSWORD intent with the
removal of the ability to set an explicit keystore password now
that the keyguard value is used. Changed to ensure keyguard is
enabled for keystore install or unlock. Cleaned up interwoven
dialog handing into discrete dialog helper classes.
AndroidManifest.xml
src/com/android/settings/CredentialStorage.java
Remove layout for entering new password
res/layout/credentials_dialog.xml
Remove enable credentials checkbox
res/xml/security_settings_misc.xml
src/com/android/settings/SecuritySettings.java
Added ability to specify minimum quality key to ChooseLockGeneric
Activity. Used by CredentialStorage, but could also be used by
CryptKeeperSettings. Changed ChooseLockGeneric to understand
minimum quality for keystore in addition to DPM and device
encryption.
src/com/android/settings/ChooseLockGeneric.java
Changed to use getActivePasswordQuality from
getKeyguardStoredPasswordQuality based on experience in
CredentialStorage. Removed bogus class javadoc.
src/com/android/settings/CryptKeeperSettings.java
Tracking KeyStore API changes
src/com/android/settings/vpn/VpnSettings.java
src/com/android/settings/wifi/WifiSettings.java
Removing now unused string resources
res/values-af/strings.xml
res/values-am/strings.xml
res/values-ar/strings.xml
res/values-bg/strings.xml
res/values-ca/strings.xml
res/values-cs/strings.xml
res/values-da/strings.xml
res/values-de/strings.xml
res/values-el/strings.xml
res/values-en-rGB/strings.xml
res/values-es-rUS/strings.xml
res/values-es/strings.xml
res/values-fa/strings.xml
res/values-fi/strings.xml
res/values-fr/strings.xml
res/values-hr/strings.xml
res/values-hu/strings.xml
res/values-in/strings.xml
res/values-it/strings.xml
res/values-iw/strings.xml
res/values-ja/strings.xml
res/values-ko/strings.xml
res/values-lt/strings.xml
res/values-lv/strings.xml
res/values-ms/strings.xml
res/values-nb/strings.xml
res/values-nl/strings.xml
res/values-pl/strings.xml
res/values-pt-rPT/strings.xml
res/values-pt/strings.xml
res/values-rm/strings.xml
res/values-ro/strings.xml
res/values-ru/strings.xml
res/values-sk/strings.xml
res/values-sl/strings.xml
res/values-sr/strings.xml
res/values-sv/strings.xml
res/values-sw/strings.xml
res/values-th/strings.xml
res/values-tl/strings.xml
res/values-tr/strings.xml
res/values-uk/strings.xml
res/values-vi/strings.xml
res/values-zh-rCN/strings.xml
res/values-zh-rTW/strings.xml
res/values-zu/strings.xml
res/values/strings.xml
packages/apps/CertInstaller
Tracking KeyStore API changes
src/com/android/certinstaller/CertInstaller.java
Fix for NPE in CertInstaller when certificate lacks basic constraints
src/com/android/certinstaller/CredentialHelper.java
packages/apps/KeyChain
Tracking KeyStore API changes
src/com/android/keychain/KeyChainActivity.java
src/com/android/keychain/KeyChainService.java
support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl
support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java
tests/src/com/android/keychain/tests/KeyChainServiceTest.java
Change-Id: I80533bf8986a92b0b99cd5fb1c4943e0f23fc1c8
1110 lines
40 KiB
Java
1110 lines
40 KiB
Java
/*
|
|
* Copyright (C) 2009 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.settings.vpn;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.SettingsPreferenceFragment;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.app.Dialog;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.net.vpn.L2tpIpsecProfile;
|
|
import android.net.vpn.L2tpIpsecPskProfile;
|
|
import android.net.vpn.L2tpProfile;
|
|
import android.net.vpn.VpnManager;
|
|
import android.net.vpn.VpnProfile;
|
|
import android.net.vpn.VpnState;
|
|
import android.net.vpn.VpnType;
|
|
import android.os.Bundle;
|
|
import android.preference.Preference;
|
|
import android.preference.PreferenceActivity;
|
|
import android.preference.PreferenceCategory;
|
|
import android.preference.PreferenceScreen;
|
|
import android.preference.Preference.OnPreferenceClickListener;
|
|
import android.security.Credentials;
|
|
import android.security.KeyStore;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.ContextMenu;
|
|
import android.view.MenuItem;
|
|
import android.view.View;
|
|
import android.view.ContextMenu.ContextMenuInfo;
|
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.ObjectInputStream;
|
|
import java.io.ObjectOutputStream;
|
|
import java.nio.charset.Charsets;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* The preference activity for configuring VPN settings.
|
|
*/
|
|
public class VpnSettings extends SettingsPreferenceFragment
|
|
implements DialogInterface.OnClickListener {
|
|
|
|
private static final boolean DEBUG = false;
|
|
|
|
// Key to the field exchanged for profile editing.
|
|
static final String KEY_VPN_PROFILE = "vpn_profile";
|
|
|
|
// Key to the field exchanged for VPN type selection.
|
|
static final String KEY_VPN_TYPE = "vpn_type";
|
|
|
|
private static final String TAG = VpnSettings.class.getSimpleName();
|
|
|
|
private static final String PREF_ADD_VPN = "add_new_vpn";
|
|
private static final String PREF_VPN_LIST = "vpn_list";
|
|
|
|
private static final String PROFILES_ROOT = VpnManager.getProfilePath() + "/";
|
|
private static final String PROFILE_OBJ_FILE = ".pobj";
|
|
|
|
private static final String KEY_ACTIVE_PROFILE = "ActiveProfile";
|
|
private static final String KEY_PROFILE_CONNECTING = "ProfileConnecting";
|
|
private static final String KEY_CONNECT_DIALOG_SHOWING = "ConnectDialogShowing";
|
|
|
|
private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1;
|
|
static final int REQUEST_SELECT_VPN_TYPE = 2;
|
|
|
|
private static final int CONTEXT_MENU_CONNECT_ID = ContextMenu.FIRST + 0;
|
|
private static final int CONTEXT_MENU_DISCONNECT_ID = ContextMenu.FIRST + 1;
|
|
private static final int CONTEXT_MENU_EDIT_ID = ContextMenu.FIRST + 2;
|
|
private static final int CONTEXT_MENU_DELETE_ID = ContextMenu.FIRST + 3;
|
|
|
|
private static final int CONNECT_BUTTON = DialogInterface.BUTTON_POSITIVE;
|
|
private static final int OK_BUTTON = DialogInterface.BUTTON_POSITIVE;
|
|
|
|
private static final int DIALOG_CONNECT = VpnManager.VPN_ERROR_LARGEST + 1;
|
|
private static final int DIALOG_SECRET_NOT_SET = DIALOG_CONNECT + 1;
|
|
|
|
private static final int NO_ERROR = VpnManager.VPN_ERROR_NO_ERROR;
|
|
|
|
private static final String KEY_PREFIX_IPSEC_PSK = Credentials.VPN + 'i';
|
|
private static final String KEY_PREFIX_L2TP_SECRET = Credentials.VPN + 'l';
|
|
|
|
private static List<VpnProfile> sVpnProfileList = new ArrayList<VpnProfile>();
|
|
|
|
private PreferenceScreen mAddVpn;
|
|
private PreferenceCategory mVpnListContainer;
|
|
|
|
// profile name --> VpnPreference
|
|
private Map<String, VpnPreference> mVpnPreferenceMap;
|
|
|
|
// profile engaged in a connection
|
|
private VpnProfile mActiveProfile;
|
|
|
|
// actor engaged in connecting
|
|
private VpnProfileActor mConnectingActor;
|
|
|
|
// states saved for unlocking keystore
|
|
private Runnable mUnlockAction;
|
|
|
|
private KeyStore mKeyStore = KeyStore.getInstance();
|
|
|
|
private VpnManager mVpnManager;
|
|
|
|
private ConnectivityReceiver mConnectivityReceiver =
|
|
new ConnectivityReceiver();
|
|
|
|
private int mConnectingErrorCode = NO_ERROR;
|
|
|
|
private Dialog mShowingDialog;
|
|
|
|
private boolean mConnectDialogShowing = false;
|
|
|
|
@Override
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
addPreferencesFromResource(R.xml.vpn_settings);
|
|
|
|
mVpnManager = new VpnManager(getActivity());
|
|
// restore VpnProfile list and construct VpnPreference map
|
|
mVpnListContainer = (PreferenceCategory) findPreference(PREF_VPN_LIST);
|
|
|
|
// set up the "add vpn" preference
|
|
mAddVpn = (PreferenceScreen) findPreference(PREF_ADD_VPN);
|
|
mAddVpn.setOnPreferenceClickListener(
|
|
new OnPreferenceClickListener() {
|
|
public boolean onPreferenceClick(Preference preference) {
|
|
startVpnTypeSelection();
|
|
return true;
|
|
}
|
|
});
|
|
|
|
retrieveVpnListFromStorage();
|
|
restoreInstanceState(savedInstanceState);
|
|
}
|
|
|
|
@Override
|
|
public void onSaveInstanceState(Bundle savedInstanceState) {
|
|
if (mActiveProfile != null) {
|
|
savedInstanceState.putString(KEY_ACTIVE_PROFILE,
|
|
mActiveProfile.getId());
|
|
savedInstanceState.putBoolean(KEY_PROFILE_CONNECTING,
|
|
(mConnectingActor != null));
|
|
savedInstanceState.putBoolean(KEY_CONNECT_DIALOG_SHOWING,
|
|
mConnectDialogShowing);
|
|
}
|
|
super.onSaveInstanceState(savedInstanceState);
|
|
}
|
|
|
|
private void restoreInstanceState(Bundle savedInstanceState) {
|
|
if (savedInstanceState == null) return;
|
|
String profileId = savedInstanceState.getString(KEY_ACTIVE_PROFILE);
|
|
if (profileId != null) {
|
|
mActiveProfile = getProfile(getProfileIndexFromId(profileId));
|
|
if (savedInstanceState.getBoolean(KEY_PROFILE_CONNECTING)) {
|
|
mConnectingActor = getActor(mActiveProfile);
|
|
}
|
|
mConnectDialogShowing = savedInstanceState.getBoolean(KEY_CONNECT_DIALOG_SHOWING);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onActivityCreated(Bundle savedInstanceState) {
|
|
super.onActivityCreated(savedInstanceState);
|
|
|
|
// for long-press gesture on a profile preference
|
|
registerForContextMenu(getListView());
|
|
}
|
|
|
|
@Override
|
|
public void onPause() {
|
|
// ignore vpn connectivity event
|
|
mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver);
|
|
if ((mShowingDialog != null) && mShowingDialog.isShowing()) {
|
|
mShowingDialog.dismiss();
|
|
mShowingDialog = null;
|
|
}
|
|
super.onPause();
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
updatePreferenceMap();
|
|
|
|
if (DEBUG) Log.d(TAG, "onResume");
|
|
|
|
// listen to vpn connectivity event
|
|
mVpnManager.registerConnectivityReceiver(mConnectivityReceiver);
|
|
|
|
if ((mUnlockAction != null) && isKeyStoreUnlocked()) {
|
|
Runnable action = mUnlockAction;
|
|
mUnlockAction = null;
|
|
getActivity().runOnUiThread(action);
|
|
}
|
|
|
|
if (!mConnectDialogShowing) {
|
|
// If mActiveProfile is not null but it's in IDLE state, then a
|
|
// retry dialog must be showing now as the previous connection
|
|
// attempt failed. In this case, don't call checkVpnConnectionStatus()
|
|
// as it will clean up mActiveProfile due to the IDLE state.
|
|
if ((mActiveProfile == null)
|
|
|| (mActiveProfile.getState() != VpnState.IDLE)) {
|
|
checkVpnConnectionStatus();
|
|
}
|
|
} else {
|
|
// Dismiss the connect dialog in case there is another instance
|
|
// trying to operate a vpn connection.
|
|
if (!mVpnManager.isIdle() || (mActiveProfile == null)) {
|
|
removeDialog(DIALOG_CONNECT);
|
|
checkVpnConnectionStatus();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDestroyView() {
|
|
unregisterForContextMenu(getListView());
|
|
// This should be called after the procedure above as ListView inside this Fragment
|
|
// will be deleted here.
|
|
super.onDestroyView();
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
// Remove any onClick listeners
|
|
if (mVpnListContainer != null) {
|
|
for (int i = 0; i < mVpnListContainer.getPreferenceCount(); i++) {
|
|
mVpnListContainer.getPreference(i).setOnPreferenceClickListener(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Dialog onCreateDialog (int id) {
|
|
setOnCancelListener(new DialogInterface.OnCancelListener() {
|
|
public void onCancel(DialogInterface dialog) {
|
|
if (mActiveProfile != null) {
|
|
changeState(mActiveProfile, VpnState.IDLE);
|
|
}
|
|
// Make sure onIdle() is called as the above changeState()
|
|
// may not be effective if the state is already IDLE.
|
|
// XXX: VpnService should broadcast non-IDLE state, say UNUSABLE,
|
|
// when an error occurs.
|
|
onIdle();
|
|
}
|
|
});
|
|
|
|
switch (id) {
|
|
case DIALOG_CONNECT:
|
|
mConnectDialogShowing = true;
|
|
setOnDismissListener(new DialogInterface.OnDismissListener() {
|
|
public void onDismiss(DialogInterface dialog) {
|
|
mConnectDialogShowing = false;
|
|
}
|
|
});
|
|
return createConnectDialog();
|
|
|
|
case DIALOG_SECRET_NOT_SET:
|
|
return createSecretNotSetDialog();
|
|
|
|
case VpnManager.VPN_ERROR_CHALLENGE:
|
|
case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
|
|
case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
|
|
return createEditDialog(id);
|
|
|
|
default:
|
|
Log.d(TAG, "create reconnect dialog for event " + id);
|
|
return createReconnectDialog(id);
|
|
}
|
|
}
|
|
|
|
private Dialog createConnectDialog() {
|
|
final Activity activity = getActivity();
|
|
return new AlertDialog.Builder(activity)
|
|
.setView(mConnectingActor.createConnectView())
|
|
.setTitle(String.format(activity.getString(R.string.vpn_connect_to),
|
|
mConnectingActor.getProfile().getName()))
|
|
.setPositiveButton(activity.getString(R.string.vpn_connect_button),
|
|
this)
|
|
.setNegativeButton(activity.getString(android.R.string.cancel),
|
|
this)
|
|
.create();
|
|
}
|
|
|
|
private Dialog createReconnectDialog(int id) {
|
|
int msgId;
|
|
switch (id) {
|
|
case VpnManager.VPN_ERROR_AUTH:
|
|
msgId = R.string.vpn_auth_error_dialog_msg;
|
|
break;
|
|
|
|
case VpnManager.VPN_ERROR_REMOTE_HUNG_UP:
|
|
msgId = R.string.vpn_remote_hung_up_error_dialog_msg;
|
|
break;
|
|
|
|
case VpnManager.VPN_ERROR_CONNECTION_LOST:
|
|
msgId = R.string.vpn_reconnect_from_lost;
|
|
break;
|
|
|
|
case VpnManager.VPN_ERROR_REMOTE_PPP_HUNG_UP:
|
|
msgId = R.string.vpn_remote_ppp_hung_up_error_dialog_msg;
|
|
break;
|
|
|
|
default:
|
|
msgId = R.string.vpn_confirm_reconnect;
|
|
}
|
|
return createCommonDialogBuilder().setMessage(msgId).create();
|
|
}
|
|
|
|
private Dialog createEditDialog(int id) {
|
|
int msgId;
|
|
switch (id) {
|
|
case VpnManager.VPN_ERROR_CHALLENGE:
|
|
msgId = R.string.vpn_challenge_error_dialog_msg;
|
|
break;
|
|
|
|
case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
|
|
msgId = R.string.vpn_unknown_server_dialog_msg;
|
|
break;
|
|
|
|
case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
|
|
msgId = R.string.vpn_ppp_negotiation_failed_dialog_msg;
|
|
break;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
return createCommonEditDialogBuilder().setMessage(msgId).create();
|
|
}
|
|
|
|
private Dialog createSecretNotSetDialog() {
|
|
return createCommonDialogBuilder()
|
|
.setMessage(R.string.vpn_secret_not_set_dialog_msg)
|
|
.setPositiveButton(R.string.vpn_yes_button,
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int w) {
|
|
startVpnEditor(mActiveProfile, false);
|
|
}
|
|
})
|
|
.create();
|
|
}
|
|
|
|
private AlertDialog.Builder createCommonEditDialogBuilder() {
|
|
return createCommonDialogBuilder()
|
|
.setPositiveButton(R.string.vpn_yes_button,
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int w) {
|
|
VpnProfile p = mActiveProfile;
|
|
onIdle();
|
|
startVpnEditor(p, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
private AlertDialog.Builder createCommonDialogBuilder() {
|
|
return new AlertDialog.Builder(getActivity())
|
|
.setTitle(android.R.string.dialog_alert_title)
|
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
|
.setPositiveButton(R.string.vpn_yes_button,
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int w) {
|
|
connectOrDisconnect(mActiveProfile);
|
|
}
|
|
})
|
|
.setNegativeButton(R.string.vpn_no_button,
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int w) {
|
|
onIdle();
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onCreateContextMenu(ContextMenu menu, View v,
|
|
ContextMenuInfo menuInfo) {
|
|
super.onCreateContextMenu(menu, v, menuInfo);
|
|
|
|
VpnProfile p = getProfile(getProfilePositionFrom(
|
|
(AdapterContextMenuInfo) menuInfo));
|
|
if (p != null) {
|
|
VpnState state = p.getState();
|
|
menu.setHeaderTitle(p.getName());
|
|
|
|
boolean isIdle = (state == VpnState.IDLE);
|
|
boolean isNotConnect = (isIdle || (state == VpnState.DISCONNECTING)
|
|
|| (state == VpnState.CANCELLED));
|
|
menu.add(0, CONTEXT_MENU_CONNECT_ID, 0, R.string.vpn_menu_connect)
|
|
.setEnabled(isIdle && (mActiveProfile == null));
|
|
menu.add(0, CONTEXT_MENU_DISCONNECT_ID, 0,
|
|
R.string.vpn_menu_disconnect)
|
|
.setEnabled(state == VpnState.CONNECTED);
|
|
menu.add(0, CONTEXT_MENU_EDIT_ID, 0, R.string.vpn_menu_edit)
|
|
.setEnabled(isNotConnect);
|
|
menu.add(0, CONTEXT_MENU_DELETE_ID, 0, R.string.vpn_menu_delete)
|
|
.setEnabled(isNotConnect);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onContextItemSelected(MenuItem item) {
|
|
int position = getProfilePositionFrom(
|
|
(AdapterContextMenuInfo) item.getMenuInfo());
|
|
VpnProfile p = getProfile(position);
|
|
|
|
switch(item.getItemId()) {
|
|
case CONTEXT_MENU_CONNECT_ID:
|
|
case CONTEXT_MENU_DISCONNECT_ID:
|
|
connectOrDisconnect(p);
|
|
return true;
|
|
|
|
case CONTEXT_MENU_EDIT_ID:
|
|
startVpnEditor(p, false);
|
|
return true;
|
|
|
|
case CONTEXT_MENU_DELETE_ID:
|
|
deleteProfile(position);
|
|
return true;
|
|
}
|
|
|
|
return super.onContextItemSelected(item);
|
|
}
|
|
|
|
@Override
|
|
public void onActivityResult(final int requestCode, final int resultCode,
|
|
final Intent data) {
|
|
|
|
if (DEBUG) Log.d(TAG, "onActivityResult , result = " + resultCode + ", data = " + data);
|
|
if ((resultCode == Activity.RESULT_CANCELED) || (data == null)) {
|
|
Log.d(TAG, "no result returned by editor");
|
|
return;
|
|
}
|
|
|
|
if (requestCode == REQUEST_SELECT_VPN_TYPE) {
|
|
final String typeName = data.getStringExtra(KEY_VPN_TYPE);
|
|
startVpnEditor(createVpnProfile(typeName), true);
|
|
} else if (requestCode == REQUEST_ADD_OR_EDIT_PROFILE) {
|
|
VpnProfile p = data.getParcelableExtra(KEY_VPN_PROFILE);
|
|
if (p == null) {
|
|
Log.e(TAG, "null object returned by editor");
|
|
return;
|
|
}
|
|
|
|
final Activity activity = getActivity();
|
|
int index = getProfileIndexFromId(p.getId());
|
|
if (checkDuplicateName(p, index)) {
|
|
final VpnProfile profile = p;
|
|
Util.showErrorMessage(activity, String.format(
|
|
activity.getString(R.string.vpn_error_duplicate_name),
|
|
p.getName()),
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int w) {
|
|
startVpnEditor(profile, false);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (needKeyStoreToSave(p)) {
|
|
Runnable action = new Runnable() {
|
|
public void run() {
|
|
onActivityResult(requestCode, resultCode, data);
|
|
}
|
|
};
|
|
if (!unlockKeyStore(p, action)) return;
|
|
}
|
|
|
|
try {
|
|
if (index < 0) {
|
|
addProfile(p);
|
|
Util.showShortToastMessage(activity, String.format(
|
|
activity.getString(R.string.vpn_profile_added), p.getName()));
|
|
} else {
|
|
replaceProfile(index, p);
|
|
Util.showShortToastMessage(activity, String.format(
|
|
activity.getString(R.string.vpn_profile_replaced),
|
|
p.getName()));
|
|
}
|
|
} catch (IOException e) {
|
|
final VpnProfile profile = p;
|
|
Util.showErrorMessage(activity, e + ": " + e.getMessage(),
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int w) {
|
|
startVpnEditor(profile, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Remove cached VpnEditor as it is needless anymore.
|
|
} else {
|
|
throw new RuntimeException("unknown request code: " + requestCode);
|
|
}
|
|
}
|
|
|
|
// Called when the buttons on the connect dialog are clicked.
|
|
@Override
|
|
public synchronized void onClick(DialogInterface dialog, int which) {
|
|
if (which == CONNECT_BUTTON) {
|
|
Dialog d = (Dialog) dialog;
|
|
String error = mConnectingActor.validateInputs(d);
|
|
if (error == null) {
|
|
mConnectingActor.connect(d);
|
|
} else {
|
|
// show error dialog
|
|
final Activity activity = getActivity();
|
|
mShowingDialog = new AlertDialog.Builder(activity)
|
|
.setTitle(android.R.string.dialog_alert_title)
|
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
|
.setMessage(String.format(activity.getString(
|
|
R.string.vpn_error_miss_entering), error))
|
|
.setPositiveButton(R.string.vpn_back_button,
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog,
|
|
int which) {
|
|
showDialog(DIALOG_CONNECT);
|
|
}
|
|
})
|
|
.create();
|
|
// The profile state is "connecting". If we allow the dialog to
|
|
// be cancelable, then we need to clear the state in the
|
|
// onCancel handler.
|
|
mShowingDialog.setCancelable(false);
|
|
mShowingDialog.show();
|
|
}
|
|
} else {
|
|
changeState(mActiveProfile, VpnState.IDLE);
|
|
}
|
|
}
|
|
|
|
private int getProfileIndexFromId(String id) {
|
|
int index = 0;
|
|
for (VpnProfile p : sVpnProfileList) {
|
|
if (p.getId().equals(id)) {
|
|
return index;
|
|
} else {
|
|
index++;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Replaces the profile at index in sVpnProfileList with p.
|
|
// Returns true if p's name is a duplicate.
|
|
private boolean checkDuplicateName(VpnProfile p, int index) {
|
|
List<VpnProfile> list = sVpnProfileList;
|
|
VpnPreference pref = mVpnPreferenceMap.get(p.getName());
|
|
if ((pref != null) && (index >= 0) && (index < list.size())) {
|
|
// not a duplicate if p is to replace the profile at index
|
|
if (pref.mProfile == list.get(index)) pref = null;
|
|
}
|
|
return (pref != null);
|
|
}
|
|
|
|
private int getProfilePositionFrom(AdapterContextMenuInfo menuInfo) {
|
|
// excludes mVpnListContainer and the preferences above it
|
|
return menuInfo.position - mVpnListContainer.getOrder() - 1;
|
|
}
|
|
|
|
// position: position in sVpnProfileList
|
|
private VpnProfile getProfile(int position) {
|
|
return ((position >= 0) ? sVpnProfileList.get(position) : null);
|
|
}
|
|
|
|
// position: position in sVpnProfileList
|
|
private void deleteProfile(final int position) {
|
|
if ((position < 0) || (position >= sVpnProfileList.size())) return;
|
|
final VpnProfile target = sVpnProfileList.get(position);
|
|
DialogInterface.OnClickListener onClickListener =
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
// Double check if the target is still the one we want
|
|
// to remove.
|
|
VpnProfile p = sVpnProfileList.get(position);
|
|
if (p != target) return;
|
|
if (which == OK_BUTTON) {
|
|
sVpnProfileList.remove(position);
|
|
VpnPreference pref =
|
|
mVpnPreferenceMap.remove(p.getName());
|
|
mVpnListContainer.removePreference(pref);
|
|
removeProfileFromStorage(p);
|
|
}
|
|
}
|
|
};
|
|
mShowingDialog = new AlertDialog.Builder(getActivity())
|
|
.setTitle(android.R.string.dialog_alert_title)
|
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
|
.setMessage(R.string.vpn_confirm_profile_deletion)
|
|
.setPositiveButton(android.R.string.ok, onClickListener)
|
|
.setNegativeButton(R.string.vpn_no_button, onClickListener)
|
|
.create();
|
|
mShowingDialog.show();
|
|
}
|
|
|
|
// Randomly generates an ID for the profile.
|
|
// The ID is unique and only set once when the profile is created.
|
|
private void setProfileId(VpnProfile profile) {
|
|
String id;
|
|
|
|
while (true) {
|
|
id = String.valueOf(Math.abs(
|
|
Double.doubleToLongBits(Math.random())));
|
|
if (id.length() >= 8) break;
|
|
}
|
|
for (VpnProfile p : sVpnProfileList) {
|
|
if (p.getId().equals(id)) {
|
|
setProfileId(profile);
|
|
return;
|
|
}
|
|
}
|
|
profile.setId(id);
|
|
}
|
|
|
|
private void addProfile(VpnProfile p) throws IOException {
|
|
setProfileId(p);
|
|
processSecrets(p);
|
|
saveProfileToStorage(p);
|
|
|
|
sVpnProfileList.add(p);
|
|
addPreferenceFor(p, true);
|
|
disableProfilePreferencesIfOneActive();
|
|
}
|
|
|
|
// Adds a preference in mVpnListContainer
|
|
private VpnPreference addPreferenceFor(
|
|
VpnProfile p, boolean addToContainer) {
|
|
VpnPreference pref = new VpnPreference(getActivity(), p);
|
|
mVpnPreferenceMap.put(p.getName(), pref);
|
|
if (addToContainer) mVpnListContainer.addPreference(pref);
|
|
|
|
pref.setOnPreferenceClickListener(
|
|
new Preference.OnPreferenceClickListener() {
|
|
public boolean onPreferenceClick(Preference pref) {
|
|
connectOrDisconnect(((VpnPreference) pref).mProfile);
|
|
return true;
|
|
}
|
|
});
|
|
return pref;
|
|
}
|
|
|
|
// index: index to sVpnProfileList
|
|
private void replaceProfile(int index, VpnProfile p) throws IOException {
|
|
Map<String, VpnPreference> map = mVpnPreferenceMap;
|
|
VpnProfile oldProfile = sVpnProfileList.set(index, p);
|
|
VpnPreference pref = map.remove(oldProfile.getName());
|
|
if (pref.mProfile != oldProfile) {
|
|
throw new RuntimeException("inconsistent state!");
|
|
}
|
|
|
|
p.setId(oldProfile.getId());
|
|
|
|
processSecrets(p);
|
|
|
|
// TODO: remove copyFiles once the setId() code propagates.
|
|
// Copy config files and remove the old ones if they are in different
|
|
// directories.
|
|
if (Util.copyFiles(getProfileDir(oldProfile), getProfileDir(p))) {
|
|
removeProfileFromStorage(oldProfile);
|
|
}
|
|
saveProfileToStorage(p);
|
|
|
|
pref.setProfile(p);
|
|
map.put(p.getName(), pref);
|
|
}
|
|
|
|
private void startVpnTypeSelection() {
|
|
if ((getActivity() == null) || isRemoving()) return;
|
|
|
|
((PreferenceActivity) getActivity()).startPreferencePanel(
|
|
VpnTypeSelection.class.getCanonicalName(), null, R.string.vpn_type_title, null,
|
|
this, REQUEST_SELECT_VPN_TYPE);
|
|
}
|
|
|
|
private boolean isKeyStoreUnlocked() {
|
|
return mKeyStore.state() == KeyStore.State.UNLOCKED;
|
|
}
|
|
|
|
// Returns true if the profile needs to access keystore
|
|
private boolean needKeyStoreToSave(VpnProfile p) {
|
|
switch (p.getType()) {
|
|
case L2TP_IPSEC_PSK:
|
|
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
|
|
String presharedKey = pskProfile.getPresharedKey();
|
|
if (!TextUtils.isEmpty(presharedKey)) return true;
|
|
// $FALL-THROUGH$
|
|
case L2TP:
|
|
L2tpProfile l2tpProfile = (L2tpProfile) p;
|
|
if (l2tpProfile.isSecretEnabled() &&
|
|
!TextUtils.isEmpty(l2tpProfile.getSecretString())) {
|
|
return true;
|
|
}
|
|
// $FALL-THROUGH$
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Returns true if the profile needs to access keystore
|
|
private boolean needKeyStoreToConnect(VpnProfile p) {
|
|
switch (p.getType()) {
|
|
case L2TP_IPSEC:
|
|
case L2TP_IPSEC_PSK:
|
|
return true;
|
|
|
|
case L2TP:
|
|
return ((L2tpProfile) p).isSecretEnabled();
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Returns true if keystore is unlocked or keystore is not a concern
|
|
private boolean unlockKeyStore(VpnProfile p, Runnable action) {
|
|
if (isKeyStoreUnlocked()) return true;
|
|
mUnlockAction = action;
|
|
Credentials.getInstance().unlock(getActivity());
|
|
return false;
|
|
}
|
|
|
|
private void startVpnEditor(final VpnProfile profile, boolean add) {
|
|
if ((getActivity() == null) || isRemoving()) return;
|
|
|
|
Bundle args = new Bundle();
|
|
args.putParcelable(KEY_VPN_PROFILE, profile);
|
|
// TODO: Show different titles for add and edit.
|
|
((PreferenceActivity)getActivity()).startPreferencePanel(
|
|
VpnEditor.class.getCanonicalName(), args,
|
|
0, VpnEditor.getTitle(getActivity(), profile, add),
|
|
this, REQUEST_ADD_OR_EDIT_PROFILE);
|
|
}
|
|
|
|
private synchronized void connect(final VpnProfile p) {
|
|
if (needKeyStoreToConnect(p)) {
|
|
Runnable action = new Runnable() {
|
|
public void run() {
|
|
connect(p);
|
|
}
|
|
};
|
|
if (!unlockKeyStore(p, action)) return;
|
|
}
|
|
|
|
if (!checkSecrets(p)) return;
|
|
changeState(p, VpnState.CONNECTING);
|
|
if (mConnectingActor.isConnectDialogNeeded()) {
|
|
showDialog(DIALOG_CONNECT);
|
|
} else {
|
|
mConnectingActor.connect(null);
|
|
}
|
|
}
|
|
|
|
// Do connect or disconnect based on the current state.
|
|
private synchronized void connectOrDisconnect(VpnProfile p) {
|
|
VpnPreference pref = mVpnPreferenceMap.get(p.getName());
|
|
switch (p.getState()) {
|
|
case IDLE:
|
|
connect(p);
|
|
break;
|
|
|
|
case CONNECTING:
|
|
// do nothing
|
|
break;
|
|
|
|
case CONNECTED:
|
|
case DISCONNECTING:
|
|
changeState(p, VpnState.DISCONNECTING);
|
|
getActor(p).disconnect();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void changeState(VpnProfile p, VpnState state) {
|
|
VpnState oldState = p.getState();
|
|
p.setState(state);
|
|
mVpnPreferenceMap.get(p.getName()).setSummary(
|
|
getProfileSummaryString(p));
|
|
|
|
switch (state) {
|
|
case CONNECTED:
|
|
mConnectingActor = null;
|
|
mActiveProfile = p;
|
|
disableProfilePreferencesIfOneActive();
|
|
break;
|
|
|
|
case CONNECTING:
|
|
if (mConnectingActor == null) {
|
|
mConnectingActor = getActor(p);
|
|
}
|
|
// $FALL-THROUGH$
|
|
case DISCONNECTING:
|
|
mActiveProfile = p;
|
|
disableProfilePreferencesIfOneActive();
|
|
break;
|
|
|
|
case CANCELLED:
|
|
changeState(p, VpnState.IDLE);
|
|
break;
|
|
|
|
case IDLE:
|
|
assert(mActiveProfile == p);
|
|
|
|
if (mConnectingErrorCode == NO_ERROR) {
|
|
onIdle();
|
|
} else {
|
|
showDialog(mConnectingErrorCode);
|
|
mConnectingErrorCode = NO_ERROR;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void onIdle() {
|
|
if (DEBUG) Log.d(TAG, " onIdle()");
|
|
mActiveProfile = null;
|
|
mConnectingActor = null;
|
|
enableProfilePreferences();
|
|
}
|
|
|
|
private void disableProfilePreferencesIfOneActive() {
|
|
if (mActiveProfile == null) return;
|
|
|
|
for (VpnProfile p : sVpnProfileList) {
|
|
switch (p.getState()) {
|
|
case CONNECTING:
|
|
case DISCONNECTING:
|
|
case IDLE:
|
|
mVpnPreferenceMap.get(p.getName()).setEnabled(false);
|
|
break;
|
|
|
|
default:
|
|
mVpnPreferenceMap.get(p.getName()).setEnabled(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void enableProfilePreferences() {
|
|
for (VpnProfile p : sVpnProfileList) {
|
|
mVpnPreferenceMap.get(p.getName()).setEnabled(true);
|
|
}
|
|
}
|
|
|
|
static String getProfileDir(VpnProfile p) {
|
|
return PROFILES_ROOT + p.getId();
|
|
}
|
|
|
|
static void saveProfileToStorage(VpnProfile p) throws IOException {
|
|
File f = new File(getProfileDir(p));
|
|
if (!f.exists()) f.mkdirs();
|
|
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
|
|
new File(f, PROFILE_OBJ_FILE)));
|
|
oos.writeObject(p);
|
|
oos.close();
|
|
}
|
|
|
|
private void removeProfileFromStorage(VpnProfile p) {
|
|
Util.deleteFile(getProfileDir(p));
|
|
}
|
|
|
|
private void updatePreferenceMap() {
|
|
mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>();
|
|
mVpnListContainer.removeAll();
|
|
for (VpnProfile p : sVpnProfileList) {
|
|
addPreferenceFor(p, true);
|
|
}
|
|
// reset the mActiveProfile if the profile has been removed from the
|
|
// other instance.
|
|
if ((mActiveProfile != null)
|
|
&& !mVpnPreferenceMap.containsKey(mActiveProfile.getName())) {
|
|
onIdle();
|
|
}
|
|
}
|
|
|
|
private void retrieveVpnListFromStorage() {
|
|
// skip the loop if the profile is loaded already.
|
|
if (sVpnProfileList.size() > 0) return;
|
|
File root = new File(PROFILES_ROOT);
|
|
String[] dirs = root.list();
|
|
if (dirs == null) return;
|
|
for (String dir : dirs) {
|
|
File f = new File(new File(root, dir), PROFILE_OBJ_FILE);
|
|
if (!f.exists()) continue;
|
|
try {
|
|
VpnProfile p = deserialize(f);
|
|
if (p == null) continue;
|
|
if (!checkIdConsistency(dir, p)) continue;
|
|
|
|
sVpnProfileList.add(p);
|
|
} catch (IOException e) {
|
|
Log.e(TAG, "retrieveVpnListFromStorage()", e);
|
|
}
|
|
}
|
|
Collections.sort(sVpnProfileList, new Comparator<VpnProfile>() {
|
|
public int compare(VpnProfile p1, VpnProfile p2) {
|
|
return p1.getName().compareTo(p2.getName());
|
|
}
|
|
});
|
|
disableProfilePreferencesIfOneActive();
|
|
}
|
|
|
|
private void checkVpnConnectionStatus() {
|
|
for (VpnProfile p : sVpnProfileList) {
|
|
changeState(p, mVpnManager.getState(p));
|
|
}
|
|
}
|
|
|
|
// A sanity check. Returns true if the profile directory name and profile ID
|
|
// are consistent.
|
|
private boolean checkIdConsistency(String dirName, VpnProfile p) {
|
|
if (!dirName.equals(p.getId())) {
|
|
Log.d(TAG, "ID inconsistent: " + dirName + " vs " + p.getId());
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private VpnProfile deserialize(File profileObjectFile) throws IOException {
|
|
try {
|
|
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
|
|
profileObjectFile));
|
|
VpnProfile p = (VpnProfile) ois.readObject();
|
|
ois.close();
|
|
return p;
|
|
} catch (ClassNotFoundException e) {
|
|
Log.d(TAG, "deserialize a profile", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private String getProfileSummaryString(VpnProfile p) {
|
|
final Activity activity = getActivity();
|
|
switch (p.getState()) {
|
|
case CONNECTING:
|
|
return activity.getString(R.string.vpn_connecting);
|
|
case DISCONNECTING:
|
|
return activity.getString(R.string.vpn_disconnecting);
|
|
case CONNECTED:
|
|
return activity.getString(R.string.vpn_connected);
|
|
default:
|
|
return activity.getString(R.string.vpn_connect_hint);
|
|
}
|
|
}
|
|
|
|
private VpnProfileActor getActor(VpnProfile p) {
|
|
return new AuthenticationActor(getActivity(), p);
|
|
}
|
|
|
|
private VpnProfile createVpnProfile(String type) {
|
|
return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type));
|
|
}
|
|
|
|
private boolean checkSecrets(VpnProfile p) {
|
|
boolean secretMissing = false;
|
|
|
|
if (p instanceof L2tpIpsecProfile) {
|
|
L2tpIpsecProfile certProfile = (L2tpIpsecProfile) p;
|
|
|
|
String cert = certProfile.getCaCertificate();
|
|
if (TextUtils.isEmpty(cert) ||
|
|
!mKeyStore.contains(Credentials.CA_CERTIFICATE + cert)) {
|
|
certProfile.setCaCertificate(null);
|
|
secretMissing = true;
|
|
}
|
|
|
|
cert = certProfile.getUserCertificate();
|
|
if (TextUtils.isEmpty(cert) ||
|
|
!mKeyStore.contains(Credentials.USER_CERTIFICATE + cert)) {
|
|
certProfile.setUserCertificate(null);
|
|
secretMissing = true;
|
|
}
|
|
}
|
|
|
|
if (p instanceof L2tpIpsecPskProfile) {
|
|
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
|
|
String presharedKey = pskProfile.getPresharedKey();
|
|
String key = KEY_PREFIX_IPSEC_PSK + p.getId();
|
|
if (TextUtils.isEmpty(presharedKey) || !mKeyStore.contains(key)) {
|
|
pskProfile.setPresharedKey(null);
|
|
secretMissing = true;
|
|
}
|
|
}
|
|
|
|
if (p instanceof L2tpProfile) {
|
|
L2tpProfile l2tpProfile = (L2tpProfile) p;
|
|
if (l2tpProfile.isSecretEnabled()) {
|
|
String secret = l2tpProfile.getSecretString();
|
|
String key = KEY_PREFIX_L2TP_SECRET + p.getId();
|
|
if (TextUtils.isEmpty(secret) || !mKeyStore.contains(key)) {
|
|
l2tpProfile.setSecretString(null);
|
|
secretMissing = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (secretMissing) {
|
|
mActiveProfile = p;
|
|
showDialog(DIALOG_SECRET_NOT_SET);
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private void processSecrets(VpnProfile p) {
|
|
switch (p.getType()) {
|
|
case L2TP_IPSEC_PSK:
|
|
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
|
|
String presharedKey = pskProfile.getPresharedKey();
|
|
String key = KEY_PREFIX_IPSEC_PSK + p.getId();
|
|
if (!TextUtils.isEmpty(presharedKey) &&
|
|
!mKeyStore.put(key, presharedKey.getBytes(Charsets.UTF_8))) {
|
|
Log.e(TAG, "keystore write failed: key=" + key);
|
|
}
|
|
pskProfile.setPresharedKey(key);
|
|
// $FALL-THROUGH$
|
|
case L2TP_IPSEC:
|
|
case L2TP:
|
|
L2tpProfile l2tpProfile = (L2tpProfile) p;
|
|
key = KEY_PREFIX_L2TP_SECRET + p.getId();
|
|
if (l2tpProfile.isSecretEnabled()) {
|
|
String secret = l2tpProfile.getSecretString();
|
|
if (!TextUtils.isEmpty(secret) &&
|
|
!mKeyStore.put(key, secret.getBytes(Charsets.UTF_8))) {
|
|
Log.e(TAG, "keystore write failed: key=" + key);
|
|
}
|
|
l2tpProfile.setSecretString(key);
|
|
} else {
|
|
mKeyStore.delete(key);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private class VpnPreference extends Preference {
|
|
VpnProfile mProfile;
|
|
VpnPreference(Context c, VpnProfile p) {
|
|
super(c);
|
|
setProfile(p);
|
|
}
|
|
|
|
void setProfile(VpnProfile p) {
|
|
mProfile = p;
|
|
setTitle(p.getName());
|
|
setSummary(getProfileSummaryString(p));
|
|
}
|
|
}
|
|
|
|
// to receive vpn connectivity events broadcast by VpnService
|
|
private class ConnectivityReceiver extends BroadcastReceiver {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
String profileName = intent.getStringExtra(
|
|
VpnManager.BROADCAST_PROFILE_NAME);
|
|
if (profileName == null) return;
|
|
|
|
VpnState s = (VpnState) intent.getSerializableExtra(
|
|
VpnManager.BROADCAST_CONNECTION_STATE);
|
|
|
|
if (s == null) {
|
|
Log.e(TAG, "received null connectivity state");
|
|
return;
|
|
}
|
|
|
|
mConnectingErrorCode = intent.getIntExtra(
|
|
VpnManager.BROADCAST_ERROR_CODE, NO_ERROR);
|
|
|
|
VpnPreference pref = mVpnPreferenceMap.get(profileName);
|
|
if (pref != null) {
|
|
Log.d(TAG, "received connectivity: " + profileName
|
|
+ ": connected? " + s
|
|
+ " err=" + mConnectingErrorCode);
|
|
// XXX: VpnService should broadcast non-IDLE state, say UNUSABLE,
|
|
// when an error occurs.
|
|
changeState(pref.mProfile, s);
|
|
} else {
|
|
Log.e(TAG, "received connectivity: " + profileName
|
|
+ ": connected? " + s + ", but profile does not exist;"
|
|
+ " just ignore it");
|
|
}
|
|
}
|
|
}
|
|
}
|