am 14f42ca9: am 110ddbfe: Merge "Fix NullPointerException issue when user change"

* commit '14f42ca9f46d2dafaadff2b0ccef645d66679bc7':
  Fix NullPointerException issue when user change
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 22eb3e8..8438efa 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -265,6 +265,8 @@
     <!-- Bluetooth settings.  Message for disconnecting from all profiles of a bluetooth device. [CHAR LIMIT=NONE] -->
     <string name="bluetooth_disconnect_all_profiles">This will end your connection with:&lt;br>&lt;b><xliff:g id="device_name">%1$s</xliff:g>&lt;/b></string>
 
+    <!-- Bluetooth broadcasting settings, option to enable/disable broadcasting -->
+    <string name="bluetooth_broadcasting">Broadcasting</string>
     <!-- Bluetooth settings.  Dialog title to disable a single profile of a device. [CHAR LIMIT=40] -->
     <string name="bluetooth_disable_profile_title">Disable profile?</string>
     <!-- Bluetooth settings.  Message for disabling a profile of a bluetooth device. [CHAR LIMIT=NONE] -->
@@ -323,6 +325,13 @@
     <string name="bluetooth_ask_lasting_discovery" product="tablet">An app wants to make your tablet visible to other Bluetooth devices. You can change this later in Bluetooth settings.</string>
     <string name="bluetooth_ask_lasting_discovery" product="default">An app wants to make your phone visible to other Bluetooth devices. You can change this later in Bluetooth settings.</string>
 
+    <!-- Strings for asking to the user whether to allow an app to start broadcasting -->
+    <string name="bluetooth_ask_start_broadcast" product="default"><xliff:g id="app_name">%1$s</xliff:g> wants to turn on Bluetooth broadcasting to communicate with other devices nearby. You can change this later in Bluetooth settings.</string>
+    <!-- Strings for asking to the user whether to allow an app to enable bluetooth and start broadcasting -->
+    <string name="bluetooth_ask_enablement_and_start_broadcast" product="default"><xliff:g id="app_name">%1$s</xliff:g> wants to turn on Bluetooth and Bluetooth broadcasting to communicate with otherdevices nearby. You can change this later in Bluetooth settings.</string>
+    <!-- Strings for bluetooth broadcasting explanation -->
+    <string name="bluetooth_broadcasting_explaination" product="default">When this feature is turned on, your phone can communicate with other devices nearby.\n\nBroadcasting uses low-power Bluetooth signals.</string>
+
     <!-- Strings for asking to the user whether to allow an app to enable bluetooth and discovery mode -->
     <string name="bluetooth_ask_enablement_and_discovery" product="tablet">An app wants to turn on Bluetooth and make your tablet visible to other devices for <xliff:g id="timeout">%1$d</xliff:g> seconds.</string>
     <!-- Strings for asking to the user whether to allow an app to enable bluetooth and discovery mode -->
@@ -1134,7 +1143,7 @@
     <!-- Bluetooth settings. Title of the advanced bluetooth settings screen [CHAR LIMIT=30]-->
     <string name="bluetooth_advanced_titlebar">Advanced Bluetooth</string>
     <!-- Bluetooth settings. Text displayed when Bluetooth is off and device list is empty [CHAR LIMIT=50]-->
-    <string name="bluetooth_empty_list_bluetooth_off">When Bluetooth is turned on, your device can communicate with other nearby Bluetooth devices.</string>
+    <string name="bluetooth_empty_list_bluetooth_off">To see devices, turn Bluetooth on.</string>
 
 
     <!-- Bluetooth settings.  The title of the screen to pick which profiles to connect to on the device.  For example, headphones may have both A2DP and headset, this allows the user to choose which one he wants to connect to. -->
diff --git a/src/com/android/settings/ConfirmLockPassword.java b/src/com/android/settings/ConfirmLockPassword.java
index d7402da..38d4a89 100644
--- a/src/com/android/settings/ConfirmLockPassword.java
+++ b/src/com/android/settings/ConfirmLockPassword.java
@@ -26,8 +26,10 @@
 import android.app.admin.DevicePolicyManager;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.CountDownTimer;
 import android.os.Handler;
 import android.preference.PreferenceActivity;
+import android.os.SystemClock;
 import android.text.Editable;
 import android.text.InputType;
 import android.text.TextWatcher;
@@ -78,7 +80,9 @@
         private PasswordEntryKeyboardHelper mKeyboardHelper;
         private PasswordEntryKeyboardView mKeyboardView;
         private Button mContinueButton;
-
+        private int mNumWrongConfirmAttempts;
+        private CountDownTimer mCountdownTimer;
+        private boolean mIsAlpha;
 
         // required constructor for fragments
         public ConfirmLockPasswordFragment() {
@@ -109,29 +113,27 @@
 
             mKeyboardView = (PasswordEntryKeyboardView) view.findViewById(R.id.keyboard);
             mHeaderText = (TextView) view.findViewById(R.id.headerText);
-            final boolean isAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == storedQuality
+            mIsAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == storedQuality
                     || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == storedQuality
                     || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == storedQuality;
-            mHeaderText.setText(isAlpha ? R.string.lockpassword_confirm_your_password_header
-                    : R.string.lockpassword_confirm_your_pin_header);
+            mHeaderText.setText(getDefaultHeader());
 
             final Activity activity = getActivity();
             mKeyboardHelper = new PasswordEntryKeyboardHelper(activity,
                     mKeyboardView, mPasswordEntry);
-            mKeyboardHelper.setKeyboardMode(isAlpha ?
+            mKeyboardHelper.setKeyboardMode(mIsAlpha ?
                     PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA
                     : PasswordEntryKeyboardHelper.KEYBOARD_MODE_NUMERIC);
             mKeyboardView.requestFocus();
 
             int currentType = mPasswordEntry.getInputType();
-            mPasswordEntry.setInputType(isAlpha ? currentType
+            mPasswordEntry.setInputType(mIsAlpha ? currentType
                     : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD));
 
             // Update the breadcrumb (title) if this is embedded in a PreferenceActivity
             if (activity instanceof PreferenceActivity) {
                 final PreferenceActivity preferenceActivity = (PreferenceActivity) activity;
-                int id = isAlpha ? R.string.lockpassword_confirm_your_password_header
-                        : R.string.lockpassword_confirm_your_pin_header;
+                int id = getDefaultHeader();
                 CharSequence title = getText(id);
                 preferenceActivity.showBreadCrumbs(title, title);
             }
@@ -139,10 +141,19 @@
             return view;
         }
 
+        private int getDefaultHeader() {
+            return mIsAlpha ? R.string.lockpassword_confirm_your_password_header
+                    : R.string.lockpassword_confirm_your_pin_header;
+        }
+
         @Override
         public void onPause() {
             super.onPause();
             mKeyboardView.requestFocus();
+            if (mCountdownTimer != null) {
+                mCountdownTimer.cancel();
+                mCountdownTimer = null;
+            }
         }
 
         @Override
@@ -150,6 +161,10 @@
             // TODO Auto-generated method stub
             super.onResume();
             mKeyboardView.requestFocus();
+            long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
+            if (deadline != 0) {
+                handleAttemptLockout(deadline);
+            }
         }
 
         private void handleNext() {
@@ -162,10 +177,40 @@
                 getActivity().setResult(RESULT_OK, intent);
                 getActivity().finish();
             } else {
-                showError(R.string.lockpattern_need_to_unlock_wrong);
+                if (++mNumWrongConfirmAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {
+                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
+                    handleAttemptLockout(deadline);
+                } else {
+                    showError(R.string.lockpattern_need_to_unlock_wrong);
+                }
             }
         }
 
+        private void handleAttemptLockout(long elapsedRealtimeDeadline) {
+            long elapsedRealtime = SystemClock.elapsedRealtime();
+            showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, 0);
+            mPasswordEntry.setEnabled(false);
+            mCountdownTimer = new CountDownTimer(
+                    elapsedRealtimeDeadline - elapsedRealtime,
+                    LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {
+
+                @Override
+                public void onTick(long millisUntilFinished) {
+                    final int secondsCountdown = (int) (millisUntilFinished / 1000);
+                    mHeaderText.setText(getString(
+                            R.string.lockpattern_too_many_failed_confirmation_attempts_footer,
+                            secondsCountdown));
+                }
+
+                @Override
+                public void onFinish() {
+                    mPasswordEntry.setEnabled(true);
+                    mHeaderText.setText(getDefaultHeader());
+                    mNumWrongConfirmAttempts = 0;
+                }
+            }.start();
+        }
+
         public void onClick(View v) {
             switch (v.getId()) {
                 case R.id.next_button:
@@ -180,14 +225,23 @@
         }
 
         private void showError(int msg) {
+            showError(msg, ERROR_MESSAGE_TIMEOUT);
+        }
+
+        private final Runnable mResetErrorRunnable = new Runnable() {
+            public void run() {
+                mHeaderText.setText(getDefaultHeader());
+            }
+        };
+
+        private void showError(int msg, long timeout) {
             mHeaderText.setText(msg);
             mHeaderText.announceForAccessibility(mHeaderText.getText());
             mPasswordEntry.setText(null);
-            mHandler.postDelayed(new Runnable() {
-                public void run() {
-                    mHeaderText.setText(R.string.lockpassword_confirm_your_password_header);
-                }
-            }, ERROR_MESSAGE_TIMEOUT);
+            mHandler.removeCallbacks(mResetErrorRunnable);
+            if (timeout != 0) {
+                mHandler.postDelayed(mResetErrorRunnable, timeout);
+            }
         }
 
         // {@link OnEditorActionListener} methods.
diff --git a/src/com/android/settings/NotificationStation.java b/src/com/android/settings/NotificationStation.java
index 10457b9..5083e27 100644
--- a/src/com/android/settings/NotificationStation.java
+++ b/src/com/android/settings/NotificationStation.java
@@ -71,6 +71,10 @@
 
     private INotificationListener.Stub mListener = new INotificationListener.Stub() {
         @Override
+        public void onListenerConnected(String[] notificationKeys) throws RemoteException {
+            // noop
+        }
+        @Override
         public void onNotificationPosted(StatusBarNotification notification) throws RemoteException {
             Log.v(TAG, "onNotificationPosted: " + notification);
             final Handler h = getListView().getHandler();
diff --git a/src/com/android/settings/applications/InstalledAppDetails.java b/src/com/android/settings/applications/InstalledAppDetails.java
index 6729ea9..f9405d4 100755
--- a/src/com/android/settings/applications/InstalledAppDetails.java
+++ b/src/com/android/settings/applications/InstalledAppDetails.java
@@ -366,11 +366,28 @@
                 mUninstallButton.setText(R.string.uninstall_text);
             }
         }
-        // If this is a device admin, it can't be uninstall or disabled.
+        // If this is a device admin, it can't be uninstalled or disabled.
         // We do this here so the text of the button is still set correctly.
         if (mDpm.packageHasActiveAdmins(mPackageInfo.packageName)) {
             enabled = false;
         }
+
+        // If this is the default (or only) home app, suppress uninstall (even if
+        // we still think it should be allowed for other reasons)
+        if (enabled && mHomePackages.contains(mPackageInfo.packageName)) {
+            ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>();
+            ComponentName currentDefaultHome  = mPm.getHomeActivities(homeActivities);
+            if (currentDefaultHome == null) {
+                // No preferred default, so permit uninstall only when
+                // there is more than one candidate
+                enabled = (mHomePackages.size() > 1);
+            } else {
+                // There is an explicit default home app -- forbid uninstall of
+                // that one, but permit it for installed-but-inactive ones.
+                enabled = !mPackageInfo.packageName.equals(currentDefaultHome.getPackageName());
+            }
+        }
+
         mUninstallButton.setEnabled(enabled);
         if (enabled) {
             // Register listener
diff --git a/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java
index b80e42a..0af9c4e 100644
--- a/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java
@@ -26,6 +26,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Bundle;
+import android.preference.PreferenceActivity;
 import android.text.Editable;
 import android.text.InputFilter;
 import android.text.TextWatcher;
@@ -179,6 +180,8 @@
             mDeviceNameEdited = false;
             mDeviceNameView.setText(mLocalAdapter.getName());
         }
+        PreferenceActivity activity = (PreferenceActivity)getActivity();
+        activity.showBreadCrumbs(mLocalAdapter.getName(), "");
     }
 
     public void afterTextChanged(Editable s) {
diff --git a/src/com/android/settings/bluetooth/BluetoothSettings.java b/src/com/android/settings/bluetooth/BluetoothSettings.java
index 529ee79..bbbeee5 100755
--- a/src/com/android/settings/bluetooth/BluetoothSettings.java
+++ b/src/com/android/settings/bluetooth/BluetoothSettings.java
@@ -360,7 +360,7 @@
         updateContent(mLocalAdapter.getBluetoothState(), false);
     }
 
-    private final View.OnClickListener mDeviceProfilesListener = new View.OnClickListener() {
+   private final View.OnClickListener mDeviceProfilesListener = new View.OnClickListener() {
         public void onClick(View v) {
             // User clicked on advanced options icon for a device in the list
             if (v.getTag() instanceof CachedBluetoothDevice) {
diff --git a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java
index 3a94865..dbfa1bc 100644
--- a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java
+++ b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java
@@ -33,6 +33,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.hardware.input.InputDeviceIdentifier;
 import android.hardware.input.InputManager;
 import android.hardware.input.KeyboardLayout;
 import android.os.Bundle;
@@ -492,9 +493,9 @@
                 if (device != null
                         && !device.isVirtual()
                         && device.isFullKeyboard()) {
-                    final String inputDeviceDescriptor = device.getDescriptor();
+                    final InputDeviceIdentifier identifier = device.getIdentifier();
                     final String keyboardLayoutDescriptor =
-                            mIm.getCurrentKeyboardLayoutForInputDevice(inputDeviceDescriptor);
+                            mIm.getCurrentKeyboardLayoutForInputDevice(identifier);
                     final KeyboardLayout keyboardLayout = keyboardLayoutDescriptor != null ?
                             mIm.getKeyboardLayout(keyboardLayoutDescriptor) : null;
 
@@ -508,7 +509,7 @@
                     pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
                         @Override
                         public boolean onPreferenceClick(Preference preference) {
-                            showKeyboardLayoutDialog(inputDeviceDescriptor);
+                            showKeyboardLayoutDialog(identifier);
                             return true;
                         }
                     });
@@ -539,19 +540,19 @@
         }
     }
 
-    private void showKeyboardLayoutDialog(String inputDeviceDescriptor) {
+    private void showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier) {
         KeyboardLayoutDialogFragment fragment =
-                new KeyboardLayoutDialogFragment(inputDeviceDescriptor);
+                new KeyboardLayoutDialogFragment(inputDeviceIdentifier);
         fragment.setTargetFragment(this, 0);
         fragment.show(getActivity().getFragmentManager(), "keyboardLayout");
     }
 
     @Override
-    public void onSetupKeyboardLayouts(String inputDeviceDescriptor) {
+    public void onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier) {
         final Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.setClass(getActivity(), KeyboardLayoutPickerActivity.class);
-        intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_DESCRIPTOR,
-                inputDeviceDescriptor);
+        intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER,
+                inputDeviceIdentifier);
         mIntentWaitingForResult = intent;
         startActivityForResult(intent, 0);
     }
@@ -561,10 +562,10 @@
         super.onActivityResult(requestCode, resultCode, data);
 
         if (mIntentWaitingForResult != null) {
-            String inputDeviceDescriptor = mIntentWaitingForResult.getStringExtra(
-                    KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_DESCRIPTOR);
+            InputDeviceIdentifier inputDeviceIdentifier = mIntentWaitingForResult
+                    .getParcelableExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER);
             mIntentWaitingForResult = null;
-            showKeyboardLayoutDialog(inputDeviceDescriptor);
+            showKeyboardLayoutDialog(inputDeviceIdentifier);
         }
     }
 
diff --git a/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java
index a232a0f..451b36e 100644
--- a/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java
+++ b/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java
@@ -30,6 +30,7 @@
 import android.content.Intent;
 import android.content.Loader;
 import android.content.res.Resources;
+import android.hardware.input.InputDeviceIdentifier;
 import android.hardware.input.InputManager;
 import android.hardware.input.KeyboardLayout;
 import android.hardware.input.InputManager.InputDeviceListener;
@@ -48,9 +49,9 @@
 
 public class KeyboardLayoutDialogFragment extends DialogFragment
         implements InputDeviceListener, LoaderCallbacks<KeyboardLayoutDialogFragment.Keyboards> {
-    private static final String KEY_INPUT_DEVICE_DESCRIPTOR = "inputDeviceDescriptor";
+    private static final String KEY_INPUT_DEVICE_IDENTIFIER = "inputDeviceIdentifier";
 
-    private String mInputDeviceDescriptor;
+    private InputDeviceIdentifier mInputDeviceIdentifier;
     private int mInputDeviceId = -1;
     private InputManager mIm;
     private KeyboardLayoutAdapter mAdapter;
@@ -58,8 +59,8 @@
     public KeyboardLayoutDialogFragment() {
     }
 
-    public KeyboardLayoutDialogFragment(String inputDeviceDescriptor) {
-        mInputDeviceDescriptor = inputDeviceDescriptor;
+    public KeyboardLayoutDialogFragment(InputDeviceIdentifier inputDeviceIdentifier) {
+        mInputDeviceIdentifier = inputDeviceIdentifier;
     }
 
     @Override
@@ -76,7 +77,7 @@
         super.onCreate(savedInstanceState);
 
         if (savedInstanceState != null) {
-            mInputDeviceDescriptor = savedInstanceState.getString(KEY_INPUT_DEVICE_DESCRIPTOR);
+            mInputDeviceIdentifier = savedInstanceState.getParcelable(KEY_INPUT_DEVICE_IDENTIFIER);
         }
 
         getLoaderManager().initLoader(0, null, this);
@@ -85,7 +86,7 @@
     @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
-        outState.putString(KEY_INPUT_DEVICE_DESCRIPTOR, mInputDeviceDescriptor);
+        outState.putParcelable(KEY_INPUT_DEVICE_IDENTIFIER, mInputDeviceIdentifier);
     }
 
     @Override
@@ -119,7 +120,8 @@
 
         mIm.registerInputDeviceListener(this, null);
 
-        InputDevice inputDevice = mIm.getInputDeviceByDescriptor(mInputDeviceDescriptor);
+        InputDevice inputDevice =
+                mIm.getInputDeviceByDescriptor(mInputDeviceIdentifier.getDescriptor());
         if (inputDevice == null) {
             dismiss();
             return;
@@ -143,7 +145,7 @@
 
     private void onSetupLayoutsButtonClicked() {
         ((OnSetupKeyboardLayoutsListener)getTargetFragment()).onSetupKeyboardLayouts(
-                mInputDeviceDescriptor);
+                mInputDeviceIdentifier);
     }
 
     @Override
@@ -156,7 +158,7 @@
         if (which >= 0 && which < mAdapter.getCount()) {
             KeyboardLayout keyboardLayout = mAdapter.getItem(which);
             if (keyboardLayout != null) {
-                mIm.setCurrentKeyboardLayoutForInputDevice(mInputDeviceDescriptor,
+                mIm.setCurrentKeyboardLayoutForInputDevice(mInputDeviceIdentifier,
                         keyboardLayout.getDescriptor());
             }
             dismiss();
@@ -165,7 +167,7 @@
 
     @Override
     public Loader<Keyboards> onCreateLoader(int id, Bundle args) {
-        return new KeyboardLayoutLoader(getActivity().getBaseContext(), mInputDeviceDescriptor);
+        return new KeyboardLayoutLoader(getActivity().getBaseContext(), mInputDeviceIdentifier);
     }
 
     @Override
@@ -289,11 +291,11 @@
     }
 
     private static final class KeyboardLayoutLoader extends AsyncTaskLoader<Keyboards> {
-        private final String mInputDeviceDescriptor;
+        private final InputDeviceIdentifier mInputDeviceIdentifier;
 
-        public KeyboardLayoutLoader(Context context, String inputDeviceDescriptor) {
+        public KeyboardLayoutLoader(Context context, InputDeviceIdentifier inputDeviceIdentifier) {
             super(context);
-            mInputDeviceDescriptor = inputDeviceDescriptor;
+            mInputDeviceIdentifier = inputDeviceIdentifier;
         }
 
         @Override
@@ -301,7 +303,7 @@
             Keyboards keyboards = new Keyboards();
             InputManager im = (InputManager)getContext().getSystemService(Context.INPUT_SERVICE);
             String[] keyboardLayoutDescriptors = im.getKeyboardLayoutsForInputDevice(
-                    mInputDeviceDescriptor);
+                    mInputDeviceIdentifier);
             for (String keyboardLayoutDescriptor : keyboardLayoutDescriptors) {
                 KeyboardLayout keyboardLayout = im.getKeyboardLayout(keyboardLayoutDescriptor);
                 if (keyboardLayout != null) {
@@ -311,7 +313,7 @@
             Collections.sort(keyboards.keyboardLayouts);
 
             String currentKeyboardLayoutDescriptor =
-                    im.getCurrentKeyboardLayoutForInputDevice(mInputDeviceDescriptor);
+                    im.getCurrentKeyboardLayoutForInputDevice(mInputDeviceIdentifier);
             if (currentKeyboardLayoutDescriptor != null) {
                 final int numKeyboardLayouts = keyboards.keyboardLayouts.size();
                 for (int i = 0; i < numKeyboardLayouts; i++) {
@@ -349,6 +351,6 @@
     }
 
     public interface OnSetupKeyboardLayoutsListener {
-        public void onSetupKeyboardLayouts(String inputDeviceDescriptor);
+        public void onSetupKeyboardLayouts(InputDeviceIdentifier mInputDeviceIdentifier);
     }
 }
\ No newline at end of file
diff --git a/src/com/android/settings/inputmethod/KeyboardLayoutPickerFragment.java b/src/com/android/settings/inputmethod/KeyboardLayoutPickerFragment.java
index 932dd10..645695e 100644
--- a/src/com/android/settings/inputmethod/KeyboardLayoutPickerFragment.java
+++ b/src/com/android/settings/inputmethod/KeyboardLayoutPickerFragment.java
@@ -20,6 +20,7 @@
 import com.android.settings.SettingsPreferenceFragment;
 
 import android.content.Context;
+import android.hardware.input.InputDeviceIdentifier;
 import android.hardware.input.InputManager;
 import android.hardware.input.InputManager.InputDeviceListener;
 import android.hardware.input.KeyboardLayout;
@@ -35,7 +36,7 @@
 
 public class KeyboardLayoutPickerFragment extends SettingsPreferenceFragment
         implements InputDeviceListener {
-    private String mInputDeviceDescriptor;
+    private InputDeviceIdentifier mInputDeviceIdentifier;
     private int mInputDeviceId = -1;
     private InputManager mIm;
     private KeyboardLayout[] mKeyboardLayouts;
@@ -46,15 +47,15 @@
      * Intent extra: The input device descriptor of the keyboard whose keyboard
      * layout is to be changed.
      */
-    public static final String EXTRA_INPUT_DEVICE_DESCRIPTOR = "input_device_descriptor";
+    public static final String EXTRA_INPUT_DEVICE_IDENTIFIER = "input_device_identifier";
 
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
-        mInputDeviceDescriptor = getActivity().getIntent().getStringExtra(
-                EXTRA_INPUT_DEVICE_DESCRIPTOR);
-        if (mInputDeviceDescriptor == null) {
+        mInputDeviceIdentifier = getActivity().getIntent().getParcelableExtra(
+                EXTRA_INPUT_DEVICE_IDENTIFIER);
+        if (mInputDeviceIdentifier == null) {
             getActivity().finish();
         }
 
@@ -70,7 +71,8 @@
 
         mIm.registerInputDeviceListener(this, null);
 
-        InputDevice inputDevice = mIm.getInputDeviceByDescriptor(mInputDeviceDescriptor);
+        InputDevice inputDevice =
+                mIm.getInputDeviceByDescriptor(mInputDeviceIdentifier.getDescriptor());
         if (inputDevice == null) {
             getActivity().finish();
             return;
@@ -97,10 +99,10 @@
             if (layout != null) {
                 boolean checked = checkboxPref.isChecked();
                 if (checked) {
-                    mIm.addKeyboardLayoutForInputDevice(mInputDeviceDescriptor,
+                    mIm.addKeyboardLayoutForInputDevice(mInputDeviceIdentifier,
                             layout.getDescriptor());
                 } else {
-                    mIm.removeKeyboardLayoutForInputDevice(mInputDeviceDescriptor,
+                    mIm.removeKeyboardLayoutForInputDevice(mInputDeviceIdentifier,
                             layout.getDescriptor());
                 }
                 return true;
@@ -143,7 +145,7 @@
 
     private void updateCheckedState() {
         String[] enabledKeyboardLayouts = mIm.getKeyboardLayoutsForInputDevice(
-                mInputDeviceDescriptor);
+                mInputDeviceIdentifier);
         Arrays.sort(enabledKeyboardLayouts);
 
         for (Map.Entry<CheckBoxPreference, KeyboardLayout> entry : mPreferenceMap.entrySet()) {
diff --git a/tests/Android.mk b/tests/Android.mk
index f54aeee..bb31539 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -5,7 +5,7 @@
 LOCAL_MODULE_TAGS := tests
 LOCAL_CERTIFICATE := platform
 
-LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_JAVA_LIBRARIES := android.test.runner bouncycastle
 
 # Include all test java files.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 53bf40f..aa7f947 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -19,6 +19,9 @@
 
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
 
     <application>
         <uses-library android:name="android.test.runner" />
diff --git a/tests/src/com/android/settings/vpn2/CertInstallerHelper.java b/tests/src/com/android/settings/vpn2/CertInstallerHelper.java
new file mode 100644
index 0000000..f95893f
--- /dev/null
+++ b/tests/src/com/android/settings/vpn2/CertInstallerHelper.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2013 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.vpn2;
+
+import android.os.Environment;
+import android.security.Credentials;
+import android.security.KeyStore;
+import android.util.Log;
+
+import com.android.internal.net.VpnProfile;
+import com.android.org.bouncycastle.asn1.ASN1InputStream;
+import com.android.org.bouncycastle.asn1.ASN1Sequence;
+import com.android.org.bouncycastle.asn1.DEROctetString;
+import com.android.org.bouncycastle.asn1.x509.BasicConstraints;
+
+import junit.framework.Assert;
+
+import libcore.io.Streams;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.KeyStore.PasswordProtection;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.PrivateKey;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * Certificate installer helper to extract information from a provided file
+ * and install certificates to keystore.
+ */
+public class CertInstallerHelper {
+    private static final String TAG = "CertInstallerHelper";
+    /* Define a password to unlock keystore after it is reset */
+    private static final String CERT_STORE_PASSWORD = "password";
+    private final int mUid = KeyStore.UID_SELF;
+    private PrivateKey mUserKey;  // private key
+    private X509Certificate mUserCert;  // user certificate
+    private List<X509Certificate> mCaCerts = new ArrayList<X509Certificate>();
+    private KeyStore mKeyStore = KeyStore.getInstance();
+
+    /**
+     * Unlock keystore and set password
+     */
+    public CertInstallerHelper() {
+        mKeyStore.reset();
+        mKeyStore.password(CERT_STORE_PASSWORD);
+    }
+
+    private void extractCertificate(String certFile, String password) {
+        InputStream in = null;
+        final byte[] raw;
+        java.security.KeyStore keystore = null;
+        try {
+            // Read .p12 file from SDCARD and extract with password
+            in = new FileInputStream(new File(
+                    Environment.getExternalStorageDirectory(), certFile));
+            raw = Streams.readFully(in);
+
+            keystore = java.security.KeyStore.getInstance("PKCS12");
+            PasswordProtection passwordProtection = new PasswordProtection(password.toCharArray());
+            keystore.load(new ByteArrayInputStream(raw), passwordProtection.getPassword());
+
+            // Install certificates and private keys
+            Enumeration<String> aliases = keystore.aliases();
+            if (!aliases.hasMoreElements()) {
+                Assert.fail("key store failed to put in keychain");
+            }
+            ArrayList<String> aliasesList = Collections.list(aliases);
+            // The keystore is initialized for each test case, there will
+            // be only one alias in the keystore
+            Assert.assertEquals(1, aliasesList.size());
+            String alias = aliasesList.get(0);
+            java.security.KeyStore.Entry entry = keystore.getEntry(alias, passwordProtection);
+            Log.d(TAG, "extracted alias = " + alias + ", entry=" + entry.getClass());
+
+            if (entry instanceof PrivateKeyEntry) {
+                Assert.assertTrue(installFrom((PrivateKeyEntry) entry));
+            }
+        } catch (IOException e) {
+            Assert.fail("Failed to read certficate: " + e);
+        } catch (KeyStoreException e) {
+            Log.e(TAG, "failed to extract certificate" + e);
+        } catch (NoSuchAlgorithmException e) {
+            Log.e(TAG, "failed to extract certificate" + e);
+        } catch (CertificateException e) {
+            Log.e(TAG, "failed to extract certificate" + e);
+        } catch (UnrecoverableEntryException e) {
+            Log.e(TAG, "failed to extract certificate" + e);
+        }
+        finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "close FileInputStream error: " + e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Extract private keys, user certificates and ca certificates
+     */
+    private synchronized boolean installFrom(PrivateKeyEntry entry) {
+        mUserKey = entry.getPrivateKey();
+        mUserCert = (X509Certificate) entry.getCertificate();
+
+        Certificate[] certs = entry.getCertificateChain();
+        Log.d(TAG, "# certs extracted = " + certs.length);
+        mCaCerts = new ArrayList<X509Certificate>(certs.length);
+        for (Certificate c : certs) {
+            X509Certificate cert = (X509Certificate) c;
+            if (isCa(cert)) {
+                mCaCerts.add(cert);
+            }
+        }
+        Log.d(TAG, "# ca certs extracted = " + mCaCerts.size());
+        return true;
+    }
+
+    private boolean isCa(X509Certificate cert) {
+        try {
+            byte[] asn1EncodedBytes = cert.getExtensionValue("2.5.29.19");
+            if (asn1EncodedBytes == null) {
+                return false;
+            }
+            DEROctetString derOctetString = (DEROctetString)
+                    new ASN1InputStream(asn1EncodedBytes).readObject();
+            byte[] octets = derOctetString.getOctets();
+            ASN1Sequence sequence = (ASN1Sequence)
+                    new ASN1InputStream(octets).readObject();
+            return BasicConstraints.getInstance(sequence).isCA();
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Extract certificate from the given file, and install it to keystore
+     * @param name certificate name
+     * @param certFile .p12 file which includes certificates
+     * @param password password to extract the .p12 file
+     */
+    public void installCertificate(VpnProfile profile, String certFile, String password) {
+        // extract private keys, certificates from the provided file
+        extractCertificate(certFile, password);
+        // install certificate to the keystore
+        int flags = KeyStore.FLAG_ENCRYPTED;
+        try {
+            if (mUserKey != null) {
+                Log.v(TAG, "has private key");
+                String key = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert;
+                byte[] value = mUserKey.getEncoded();
+
+                if (!mKeyStore.importKey(key, value, mUid, flags)) {
+                    Log.e(TAG, "Failed to install " + key + " as user " + mUid);
+                    return;
+                }
+                Log.v(TAG, "install " + key + " as user " + mUid + " is successful");
+            }
+
+            if (mUserCert != null) {
+                String certName = Credentials.USER_CERTIFICATE + profile.ipsecUserCert;
+                byte[] certData = Credentials.convertToPem(mUserCert);
+
+                if (!mKeyStore.put(certName, certData, mUid, flags)) {
+                    Log.e(TAG, "Failed to install " + certName + " as user " + mUid);
+                    return;
+                }
+                Log.v(TAG, "install " + certName + " as user" + mUid + " is successful.");
+            }
+
+            if (!mCaCerts.isEmpty()) {
+                String caListName = Credentials.CA_CERTIFICATE + profile.ipsecCaCert;
+                X509Certificate[] caCerts = mCaCerts.toArray(new X509Certificate[mCaCerts.size()]);
+                byte[] caListData = Credentials.convertToPem(caCerts);
+
+                if (!mKeyStore.put(caListName, caListData, mUid, flags)) {
+                    Log.e(TAG, "Failed to install " + caListName + " as user " + mUid);
+                    return;
+                }
+                Log.v(TAG, " install " + caListName + " as user " + mUid + " is successful");
+            }
+        } catch (CertificateEncodingException e) {
+            Log.e(TAG, "Exception while convert certificates to pem " + e);
+            throw new AssertionError(e);
+        } catch (IOException e) {
+            Log.e(TAG, "IOException while convert to pem: " + e);
+        }
+    }
+
+    public int getUid() {
+        return mUid;
+    }
+}
diff --git a/tests/src/com/android/settings/vpn2/VpnInfo.java b/tests/src/com/android/settings/vpn2/VpnInfo.java
new file mode 100644
index 0000000..ab7fb0f
--- /dev/null
+++ b/tests/src/com/android/settings/vpn2/VpnInfo.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 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.vpn2;
+
+import com.android.internal.net.VpnProfile;
+
+/**
+ * Wrapper for VPN Profile and associated certificate files
+ */
+public class VpnInfo {
+    // VPN Profile
+    private VpnProfile mVpnProfile;
+    // Certificate file in PC12 format for user certificates and private keys
+    private String mCertificateFile = null;
+    // Password to extract certificates from the file
+    private String mPassword = null;
+
+    public VpnInfo(VpnProfile vpnProfile, String certFile, String password) {
+        mVpnProfile = vpnProfile;
+        mCertificateFile = certFile;
+        mPassword = password;
+    }
+
+    public VpnInfo(VpnProfile vpnProfile) {
+        mVpnProfile = vpnProfile;
+    }
+
+    public void setVpnProfile(VpnProfile vpnProfile) {
+        mVpnProfile = vpnProfile;
+    }
+
+    public void setCertificateFile(String certFile) {
+        mCertificateFile = certFile;
+    }
+
+    public void setPassword(String password) {
+        mPassword = password;
+    }
+
+    public VpnProfile getVpnProfile() {
+        return mVpnProfile;
+    }
+
+    public String getCertificateFile() {
+        return mCertificateFile;
+    }
+
+    public String getPassword() {
+        return mPassword;
+    }
+}
diff --git a/tests/src/com/android/settings/vpn2/VpnProfileParser.java b/tests/src/com/android/settings/vpn2/VpnProfileParser.java
new file mode 100644
index 0000000..51c2550
--- /dev/null
+++ b/tests/src/com/android/settings/vpn2/VpnProfileParser.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2013 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.vpn2;
+
+import android.util.Log;
+
+import com.android.internal.net.VpnProfile;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Parse VPN profiles from an XML file
+ */
+public class VpnProfileParser {
+    private final static String TAG = "VpnProfileParser";
+    private static Map<Integer, VpnInfo> mVpnPool = new HashMap<Integer, VpnInfo>();
+
+    static DefaultHandler mHandler = new DefaultHandler() {
+        boolean name;
+        boolean type;
+        boolean server;
+        boolean username;
+        boolean password;
+        boolean dnsServers;
+        boolean searchDomains;
+        boolean routes;
+        boolean mppe;
+        boolean l2tpSecret;
+        boolean ipsecIdentifier;
+        boolean ipsecSecret;
+        boolean ipsecUserCert;
+        boolean ipsecCaCert;
+        boolean ipsecServerCert;
+        boolean certFile;
+        boolean certFilePassword;
+        VpnProfile profile = null;
+        VpnInfo vpnInfo = null;
+
+
+        @Override
+        public void startElement(String uri, String localName, String tagName,
+                Attributes attributes) throws SAXException {
+            if (tagName.equalsIgnoreCase("vpn")) {
+                //create a new VPN profile
+                profile = new VpnProfile(Long.toHexString(System.currentTimeMillis()));
+                vpnInfo = new VpnInfo(profile);
+            }
+            if (tagName.equalsIgnoreCase("name")) {
+                name = true;
+            }
+            if (tagName.equalsIgnoreCase("type")) {
+                type = true;
+            }
+            if (tagName.equalsIgnoreCase("server")) {
+                server = true;
+            }
+            if (tagName.equalsIgnoreCase("username")) {
+                username = true;
+            }
+            if (tagName.equalsIgnoreCase("password")) {
+                password = true;
+            }
+            if (tagName.equalsIgnoreCase("dnsServers")) {
+                dnsServers = true;
+            }
+            if (tagName.equalsIgnoreCase("searchDomains")) {
+                searchDomains = true;
+            }
+            if (tagName.equalsIgnoreCase("mppe")) {
+                mppe = true;
+            }
+            if (tagName.equalsIgnoreCase("l2tpSecret")) {
+                l2tpSecret = true;
+            }
+            if (tagName.equalsIgnoreCase("ipsecIdentifier")) {
+                ipsecIdentifier = true;
+            }
+            if (tagName.equalsIgnoreCase("ipsecSecret")) {
+                ipsecSecret = true;
+            }
+            if (tagName.equalsIgnoreCase("ipsecUserCert")) {
+                ipsecUserCert = true;
+            }
+            if (tagName.equalsIgnoreCase("ipsecCaCert")) {
+                ipsecCaCert = true;
+            }
+            if (tagName.equalsIgnoreCase("ipsecServerCert")) {
+                ipsecServerCert = true;
+            }
+            if (tagName.equalsIgnoreCase("routes")) {
+                routes = true;
+            }
+            if (tagName.equalsIgnoreCase("cert-file")) {
+                certFile = true;
+            }
+            if (tagName.equalsIgnoreCase("cert-file-password")) {
+                certFilePassword = true;
+            }
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String tagName) throws SAXException {
+            if (tagName.equalsIgnoreCase("vpn")) {
+                mVpnPool.put(profile.type, vpnInfo);
+            }
+        }
+
+        @Override
+        public void characters(char ch[], int start, int length) throws SAXException {
+            String strValue = new String(ch, start, length);
+            if (name) {
+                profile.name = strValue;
+                name = false;
+            }
+            if (type) {
+                int t = getVpnProfileType(strValue);
+                if (t < 0) {
+                    throw new SAXException("not a valid VPN type");
+                } else {
+                    profile.type = t;
+                }
+                type = false;
+            }
+            if (server) {
+                profile.server = strValue;
+                server = false;
+            }
+            if (username) {
+                profile.username = strValue;
+                username = false;
+            }
+            if (password) {
+                profile.password = strValue;
+                password = false;
+            }
+            if (dnsServers) {
+                profile.dnsServers = strValue;
+                dnsServers = false;
+            }
+            if (searchDomains) {
+                profile.searchDomains = strValue;
+                searchDomains = false;
+            }
+            if (mppe) {
+                profile.mppe = Boolean.valueOf(strValue);
+                mppe = false;
+            }
+            if (l2tpSecret) {
+                profile.l2tpSecret = strValue;
+                l2tpSecret = false;
+            }
+            if (ipsecIdentifier) {
+                profile.ipsecIdentifier = strValue;
+                ipsecIdentifier = false;
+            }
+            if (ipsecSecret) {
+                profile.ipsecSecret = strValue;
+                ipsecSecret = false;
+            }
+            if (ipsecUserCert) {
+                profile.ipsecUserCert = strValue;
+                ipsecUserCert = false;
+            }
+            if (ipsecCaCert) {
+                profile.ipsecCaCert = strValue;
+                ipsecCaCert = false;
+            }
+            if (ipsecServerCert) {
+                profile.ipsecServerCert = strValue;
+                ipsecServerCert = false;
+            }
+            if (routes) {
+                profile.routes = strValue;
+                routes = false;
+            }
+            if (certFile) {
+                vpnInfo.setCertificateFile(strValue);
+                certFile = false;
+            }
+            if (certFilePassword) {
+                vpnInfo.setPassword(strValue);
+                certFilePassword = false;
+            }
+        }
+
+        private int getVpnProfileType(String type) {
+            if (type.equalsIgnoreCase("TYPE_PPTP")) {
+                return VpnProfile.TYPE_PPTP;
+            } else if (type.equalsIgnoreCase("TYPE_L2TP_IPSEC_PSK")) {
+                return VpnProfile.TYPE_L2TP_IPSEC_PSK;
+            } else if (type.equalsIgnoreCase("TYPE_L2TP_IPSEC_RSA")) {
+                return VpnProfile.TYPE_L2TP_IPSEC_RSA;
+            } else if (type.equalsIgnoreCase("TYPE_IPSEC_XAUTH_PSK")) {
+                return VpnProfile.TYPE_IPSEC_XAUTH_PSK;
+            } else if (type.equalsIgnoreCase("TYPE_IPSEC_XAUTH_RSA")) {
+                return VpnProfile.TYPE_IPSEC_XAUTH_RSA;
+            } else if (type.equalsIgnoreCase("TYPE_IPSEC_HYBRID_RSA")) {
+                return VpnProfile.TYPE_IPSEC_HYBRID_RSA;
+            } else {
+                Log.v(TAG, "Invalid VPN type: " + type);
+                return -1;
+            }
+        }
+    };
+
+    public static Map<Integer, VpnInfo> parse(InputStream in) {
+        try {
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            SAXParser saxParser = factory.newSAXParser();
+            saxParser.parse(in, mHandler);
+        } catch (SAXException e) {
+            Log.e(TAG, "Parse vpn profile exception: " + e.toString());
+        } catch (IOException e) {
+            Log.e(TAG, "Parse vpn profile exception: " + e.toString());
+        } catch (ParserConfigurationException e) {
+            Log.e(TAG, "Parse vpn profile exception: " + e.toString());
+        } finally {
+            return mVpnPool;
+        }
+    }
+}
diff --git a/tests/src/com/android/settings/vpn2/VpnTests.java b/tests/src/com/android/settings/vpn2/VpnTests.java
new file mode 100644
index 0000000..7d5961d
--- /dev/null
+++ b/tests/src/com/android/settings/vpn2/VpnTests.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2013 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.vpn2;
+
+import android.content.Context;
+import android.net.IConnectivityManager;
+import android.net.LinkAddress;
+import android.net.RouteInfo;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.security.Credentials;
+import android.security.KeyStore;
+import android.test.InstrumentationTestCase;
+import android.test.InstrumentationTestRunner;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
+
+import junit.framework.Assert;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Legacy VPN connection tests
+ *
+ * To run the test, use command:
+ * adb shell am instrument -e class com.android.settings.vpn2.VpnTests -e profile foo.xml
+ * -w com.android.settings.tests/android.test.InstrumentationTestRunner
+ *
+ * VPN profiles are saved in an xml file and will be loaded through {@link VpnProfileParser}.
+ * Push the profile (foo.xml) to the external storage, e.g adb push foo.xml /sdcard/ before running
+ * the above command.
+ *
+ * A typical profile looks like the following:
+ * <vpn>
+ *   <name></name>
+ *   <type></type>
+ *   <server></server>
+ *   <username></username>
+ *   <password></password>
+ *   <dnsServers></dnsServers>
+ *   <searchDomains></searchDomains>
+ *   <routes></routes>
+ *   <l2tpSecret></l2tpSecret>
+ *   <ipsecIdentifier></ipsecIdentifier>
+ *   <ipsecSecret></ipsecSecret>
+ *   <ipsecUserCert></ipsecUserCert>
+ *   <ipsecCaCert></ipsecCaCert>
+ *   <ipsecServerCert></ipsecServerCert>
+ * </vpn>
+ * VPN types include: TYPE_PPTP, TYPE_L2TP_IPSEC_PSK, TYPE_L2TP_IPSEC_RSA,
+ * TYPE_IPSEC_XAUTH_PSK, TYPE_IPSEC_XAUTH_RSA, TYPE_IPSEC_HYBRID_RSA
+ */
+public class VpnTests extends InstrumentationTestCase {
+    private static final String TAG = "VpnTests";
+    /* Maximum time to wait for VPN connection */
+    private static final long MAX_CONNECTION_TIME = 5 * 60 * 1000;
+    private static final long VPN_STAY_TIME = 60 * 1000;
+    private static final int MAX_DISCONNECTION_TRIES = 3;
+    private static final String EXTERNAL_SERVER =
+            "http://ip2country.sourceforge.net/ip2c.php?format=JSON";
+    private static final String VPN_INTERFACE = "ppp0";
+    private final IConnectivityManager mService = IConnectivityManager.Stub
+        .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
+    private Map<Integer, VpnInfo> mVpnInfoPool = null;
+    private Context mContext;
+    private CertInstallerHelper mCertHelper = null;
+    private KeyStore mKeyStore = KeyStore.getInstance();
+    private String mPreviousIpAddress = null;
+    private boolean DEBUG = false;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        InputStream in = null;
+        InstrumentationTestRunner mRunner = (InstrumentationTestRunner)getInstrumentation();
+        mContext = mRunner.getContext();
+        Bundle arguments = mRunner.getArguments();
+        String PROFILE_NAME = arguments.getString("profile");
+        Assert.assertNotNull("Push profile to external storage and load with"
+                + "'-e profile <filename>'", PROFILE_NAME);
+        File profileFile = new File(Environment.getExternalStorageDirectory(), PROFILE_NAME);
+        in = new FileInputStream(profileFile);
+        mVpnInfoPool = VpnProfileParser.parse(in);
+        Assert.assertNotNull("no VPN profiles are parsed", mVpnInfoPool);
+        if (DEBUG) {
+            Log.v(TAG, "print out the vpn profiles");
+            for (Map.Entry<Integer, VpnInfo> profileEntrySet: mVpnInfoPool.entrySet()) {
+                VpnInfo vpnInfo = profileEntrySet.getValue();
+                printVpnProfile(vpnInfo.getVpnProfile());
+                if (vpnInfo.getCertificateFile() != null) {
+                    Log.d(TAG, "certificate file for this vpn is " + vpnInfo.getCertificateFile());
+                }
+                if (vpnInfo.getPassword() != null) {
+                    Log.d(TAG, "password for the certificate file is: " + vpnInfo.getPassword());
+                }
+            }
+        }
+        // disconnect existing vpn if there is any
+        LegacyVpnInfo oldVpn = mService.getLegacyVpnInfo();
+        if (oldVpn != null) {
+            Log.v(TAG, "disconnect legacy VPN");
+            disconnect();
+            // wait till the legacy VPN is disconnected.
+            int tries = 0;
+            while (tries < MAX_DISCONNECTION_TRIES && mService.getLegacyVpnInfo() != null) {
+                tries++;
+                Thread.sleep(10 * 1000);
+                Log.v(TAG, "Wait for legacy VPN to be disconnected.");
+            }
+            Assert.assertNull("Failed to disconect VPN", mService.getLegacyVpnInfo());
+            // wait for 30 seconds after the previous VPN is disconnected.
+            sleep(30 * 1000);
+        }
+        // Create CertInstallerHelper to initialize the keystore
+        mCertHelper = new CertInstallerHelper();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        sleep(VPN_STAY_TIME);
+        super.tearDown();
+    }
+
+    private void printVpnProfile(VpnProfile profile) {
+        Log.v(TAG, "profile: ");
+        Log.v(TAG, "key: " + profile.key);
+        Log.v(TAG, "name: " + profile.name);
+        Log.v(TAG, "type: " + profile.type);
+        Log.v(TAG, "server: " + profile.server);
+        Log.v(TAG, "username: " + profile.username);
+        Log.v(TAG, "password: " + profile.password);
+        Log.v(TAG, "dnsServers: " + profile.dnsServers);
+        Log.v(TAG, "searchDomains: " + profile.searchDomains);
+        Log.v(TAG, "routes: " + profile.routes);
+        Log.v(TAG, "mppe: " + profile.mppe);
+        Log.v(TAG, "l2tpSecret: " + profile.l2tpSecret);
+        Log.v(TAG, "ipsecIdentifier: " + profile.ipsecIdentifier);
+        Log.v(TAG, "ipsecSecret: " + profile.ipsecSecret);
+        Log.v(TAG, "ipsecUserCert: " + profile.ipsecUserCert);
+        Log.v(TAG, "ipsecCaCert: " + profile.ipsecCaCert);
+        Log.v(TAG, "ipsecServerCert: " + profile.ipsecServerCert);
+    }
+
+    private void printKeyStore(VpnProfile profile) {
+        // print out the information from keystore
+        String privateKey = "";
+        String userCert = "";
+        String caCert = "";
+        String serverCert = "";
+        if (!profile.ipsecUserCert.isEmpty()) {
+            privateKey = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert;
+            byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert);
+            userCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
+        }
+        if (!profile.ipsecCaCert.isEmpty()) {
+            byte[] value = mKeyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert);
+            caCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
+        }
+        if (!profile.ipsecServerCert.isEmpty()) {
+            byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert);
+            serverCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
+        }
+        Log.v(TAG, "privateKey: \n" + ((privateKey == null) ? "" : privateKey));
+        Log.v(TAG, "userCert: \n" + ((userCert == null) ? "" : userCert));
+        Log.v(TAG, "caCert: \n" + ((caCert == null) ? "" : caCert));
+        Log.v(TAG, "serverCert: \n" + ((serverCert == null) ? "" : serverCert));
+    }
+
+    /**
+     * Connect legacy VPN
+     */
+    private void connect(VpnProfile profile) throws Exception {
+        try {
+            mService.startLegacyVpn(profile);
+        } catch (IllegalStateException e) {
+            fail(String.format("start legacy vpn: %s failed: %s", profile.name, e.toString()));
+        }
+    }
+
+    /**
+     * Disconnect legacy VPN
+     */
+    private void disconnect() throws Exception {
+        try {
+            mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
+        } catch (RemoteException e) {
+            Log.e(TAG, String.format("disconnect VPN exception: %s", e.toString()));
+        }
+    }
+
+    /**
+     * Get external IP address
+     */
+    private String getIpAddress() {
+        String ip = null;
+        try {
+            HttpClient httpClient = new DefaultHttpClient();
+            HttpGet httpGet = new HttpGet(EXTERNAL_SERVER);
+            HttpResponse httpResponse = httpClient.execute(httpGet);
+            Log.i(TAG, "Response from httpget: " + httpResponse.getStatusLine().toString());
+
+            String entityStr = EntityUtils.toString(httpResponse.getEntity());
+            JSONObject json_data = new JSONObject(entityStr);
+            ip = json_data.getString("ip");
+            Log.v(TAG, "json_data: " + ip);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "exception while getting external IP: " + e.toString());
+        } catch (IOException e) {
+            Log.e(TAG, "IOException while getting IP: " + e.toString());
+        } catch (JSONException e) {
+            Log.e(TAG, "exception while creating JSONObject: " + e.toString());
+        }
+        return ip;
+    }
+
+    /**
+     * Verify the vpn connection by checking the VPN state and external IP
+     */
+    private void validateVpnConnection(VpnProfile profile) throws Exception {
+        validateVpnConnection(profile, false);
+    }
+
+    /**
+     * Verify the vpn connection by checking the VPN state, external IP or ping test
+     */
+    private void validateVpnConnection(VpnProfile profile, boolean pingTestFlag) throws Exception {
+        LegacyVpnInfo legacyVpnInfo = mService.getLegacyVpnInfo();
+        Assert.assertTrue(legacyVpnInfo != null);
+
+        long start = System.currentTimeMillis();
+        while (((System.currentTimeMillis() - start)  < MAX_CONNECTION_TIME) &&
+                (legacyVpnInfo.state != LegacyVpnInfo.STATE_CONNECTED)) {
+            Log.v(TAG, "vpn state: " + legacyVpnInfo.state);
+            sleep(10 * 1000);
+            legacyVpnInfo = mService.getLegacyVpnInfo();
+        }
+
+        // the vpn state should be CONNECTED
+        Assert.assertTrue(legacyVpnInfo.state == LegacyVpnInfo.STATE_CONNECTED);
+        if (pingTestFlag) {
+            Assert.assertTrue(pingTest(profile.server));
+        } else {
+            String curIpAddress = getIpAddress();
+            // the outgoing IP address should be the same as the VPN server address
+            Assert.assertEquals(profile.server, curIpAddress);
+        }
+    }
+
+    private boolean pingTest(String server) {
+        final long PING_TIMER = 3 * 60 * 1000; // 3 minutes
+        if (server == null || server.isEmpty()) {
+            return false;
+        }
+        long startTime = System.currentTimeMillis();
+        while ((System.currentTimeMillis() - startTime) < PING_TIMER) {
+            try {
+                Log.v(TAG, "Start ping test, ping " + server);
+                Process p = Runtime.getRuntime().exec("ping -c 10 -w 100 " + server);
+                int status = p.waitFor();
+                if (status == 0) {
+                    // if any of the ping test is successful, return true
+                    return true;
+                }
+            } catch (UnknownHostException e) {
+                Log.e(TAG, "Ping test Fail: Unknown Host");
+            } catch (IOException e) {
+                Log.e(TAG, "Ping test Fail:  IOException");
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Ping test Fail: InterruptedException");
+            }
+        }
+        // ping test timeout
+        return false;
+    }
+
+    /**
+     * Install certificates from a file loaded in external stroage on the device
+     * @param profile vpn profile
+     * @param fileName certificate file name
+     * @param password password to extract certificate file
+     */
+    private void installCertificatesFromFile(VpnProfile profile, String fileName, String password)
+            throws Exception {
+        if (profile == null || fileName == null || password == null) {
+            throw new Exception ("vpn profile, certificate file name and password can not be null");
+        }
+
+        int curUid = mContext.getUserId();
+        mCertHelper.installCertificate(profile, fileName, password);
+
+        if (DEBUG) {
+            printKeyStore(profile);
+        }
+    }
+
+    private void sleep(long time) {
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "interrupted: " + e.toString());
+        }
+    }
+
+    /**
+     * Test PPTP VPN connection
+     */
+    @LargeTest
+    public void testPPTPConnection() throws Exception {
+        mPreviousIpAddress = getIpAddress();
+        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_PPTP);
+        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
+        connect(vpnProfile);
+        validateVpnConnection(vpnProfile);
+    }
+
+    /**
+     * Test L2TP/IPSec PSK VPN connection
+     */
+    @LargeTest
+    public void testL2tpIpsecPskConnection() throws Exception {
+        mPreviousIpAddress = getIpAddress();
+        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_L2TP_IPSEC_PSK);
+        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
+        connect(vpnProfile);
+        validateVpnConnection(vpnProfile);
+    }
+
+    /**
+     * Test L2TP/IPSec RSA VPN connection
+     */
+    @LargeTest
+    public void testL2tpIpsecRsaConnection() throws Exception {
+        mPreviousIpAddress = getIpAddress();
+        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_L2TP_IPSEC_RSA);
+        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
+        if (DEBUG) {
+            printVpnProfile(vpnProfile);
+        }
+        String certFile = curVpnInfo.getCertificateFile();
+        String password = curVpnInfo.getPassword();
+        installCertificatesFromFile(vpnProfile, certFile, password);
+        connect(vpnProfile);
+        validateVpnConnection(vpnProfile);
+    }
+
+    /**
+     * Test IPSec Xauth RSA VPN connection
+     */
+    @LargeTest
+    public void testIpsecXauthRsaConnection() throws Exception {
+        mPreviousIpAddress = getIpAddress();
+        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_XAUTH_RSA);
+        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
+        if (DEBUG) {
+            printVpnProfile(vpnProfile);
+        }
+        String certFile = curVpnInfo.getCertificateFile();
+        String password = curVpnInfo.getPassword();
+        installCertificatesFromFile(vpnProfile, certFile, password);
+        connect(vpnProfile);
+        validateVpnConnection(vpnProfile);
+    }
+
+    /**
+     * Test IPSec Xauth PSK VPN connection
+     */
+    @LargeTest
+    public void testIpsecXauthPskConnection() throws Exception {
+        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_XAUTH_PSK);
+        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
+        if (DEBUG) {
+            printVpnProfile(vpnProfile);
+        }
+        connect(vpnProfile);
+        validateVpnConnection(vpnProfile, true);
+    }
+
+    /**
+     * Test IPSec Hybrid RSA VPN connection
+     */
+    @LargeTest
+    public void testIpsecHybridRsaConnection() throws Exception {
+        mPreviousIpAddress = getIpAddress();
+        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_HYBRID_RSA);
+        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
+        if (DEBUG) {
+            printVpnProfile(vpnProfile);
+        }
+        connect(vpnProfile);
+        validateVpnConnection(vpnProfile);
+    }
+}